Skip to main content

alef_e2e/codegen/
go.rs

1//! Go e2e test generator using testing.T.
2
3use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::Language;
10use alef_core::config::ResolvedCrateConfig;
11use alef_core::hash::{self, CommentStyle};
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::fmt::Write as FmtWrite;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use super::client;
19
20/// Go e2e code generator.
21pub struct GoCodegen;
22
23impl E2eCodegen for GoCodegen {
24    fn generate(
25        &self,
26        groups: &[FixtureGroup],
27        e2e_config: &E2eConfig,
28        config: &ResolvedCrateConfig,
29        _type_defs: &[alef_core::ir::TypeDef],
30        _enums: &[alef_core::ir::EnumDef],
31    ) -> Result<Vec<GeneratedFile>> {
32        let lang = self.language_name();
33        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35        let mut files = Vec::new();
36
37        // Resolve call config with overrides (for module path and import alias).
38        let call = &e2e_config.call;
39        let overrides = call.overrides.get(lang);
40        let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
41        let module_path = overrides
42            .and_then(|o| o.module.as_ref())
43            .cloned()
44            .or_else(|| configured_go_module_path.clone())
45            .unwrap_or_else(|| call.module.clone());
46        let import_alias = overrides
47            .and_then(|o| o.alias.as_ref())
48            .cloned()
49            .unwrap_or_else(|| "pkg".to_string());
50
51        // Resolve package config.
52        let go_pkg = e2e_config.resolve_package("go");
53        let go_module_path = go_pkg
54            .as_ref()
55            .and_then(|p| p.module.as_ref())
56            .cloned()
57            .or_else(|| configured_go_module_path.clone())
58            .unwrap_or_else(|| module_path.clone());
59        let replace_path = go_pkg
60            .as_ref()
61            .and_then(|p| p.path.as_ref())
62            .cloned()
63            .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
64        let go_version = go_pkg
65            .as_ref()
66            .and_then(|p| p.version.as_ref())
67            .cloned()
68            .unwrap_or_else(|| {
69                config
70                    .resolved_version()
71                    .map(|v| format!("v{v}"))
72                    .unwrap_or_else(|| "v0.0.0".to_string())
73            });
74        // Generate go.mod. In registry mode, omit the `replace` directive so the
75        // module is fetched from the Go module proxy.
76        let effective_replace = match e2e_config.dep_mode {
77            crate::config::DependencyMode::Registry => None,
78            crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
79        };
80        // In local mode with a `replace` directive the version in `require` is a
81        // placeholder.  Go requires that for a major-version module path (`/vN`, N ≥ 2)
82        // the placeholder version must start with `vN.`, e.g. `v3.0.0`.  A version like
83        // `v0.0.0` is rejected with "should be v3, not v0".  Fix the placeholder when the
84        // module path ends with `/vN` and the configured version doesn't match.
85        let effective_go_version = if effective_replace.is_some() {
86            fix_go_major_version(&go_module_path, &go_version)
87        } else {
88            go_version.clone()
89        };
90        files.push(GeneratedFile {
91            path: output_base.join("go.mod"),
92            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
93            generated_header: false,
94        });
95
96        // Determine if any fixture needs jsonString helper across all groups.
97        let emits_executable_test =
98            |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
99        let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
100            emits_executable_test(f)
101                && f.assertions.iter().any(|a| {
102                    matches!(
103                        a.assertion_type.as_str(),
104                        "contains" | "contains_all" | "contains_any" | "not_contains"
105                    ) && {
106                        if a.field.as_ref().is_none_or(|f| f.is_empty()) {
107                            e2e_config
108                                .resolve_call_for_fixture(
109                                    f.call.as_deref(),
110                                    &f.id,
111                                    &f.resolved_category(),
112                                    &f.tags,
113                                    &f.input,
114                                )
115                                .result_is_array
116                        } else {
117                            let cc = e2e_config.resolve_call_for_fixture(
118                                f.call.as_deref(),
119                                &f.id,
120                                &f.resolved_category(),
121                                &f.tags,
122                                &f.input,
123                            );
124                            let per_call_resolver = FieldResolver::new(
125                                e2e_config.effective_fields(cc),
126                                e2e_config.effective_fields_optional(cc),
127                                e2e_config.effective_result_fields(cc),
128                                e2e_config.effective_fields_array(cc),
129                                &std::collections::HashSet::new(),
130                            );
131                            let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
132                            per_call_resolver.is_array(resolved_name)
133                        }
134                    }
135                })
136        });
137
138        // Generate helpers_test.go with jsonString if needed, emitted exactly once.
139        if needs_json_stringify {
140            files.push(GeneratedFile {
141                path: output_base.join("helpers_test.go"),
142                content: render_helpers_test_go(),
143                generated_header: true,
144            });
145        }
146
147        // Generate main_test.go with TestMain when:
148        // 1. Any fixture needs the mock server (has mock_response), or
149        // 2. Any fixture is client_factory-based (reads MOCK_SERVER_URL), or
150        // 3. Any fixture is file-based (requires test_documents directory setup).
151        //
152        // TestMain runs before all tests and changes to the test_documents directory,
153        // ensuring that relative file paths like "pdf/fake_memo.pdf" resolve correctly.
154        let has_file_fixtures = groups
155            .iter()
156            .flat_map(|g| g.fixtures.iter())
157            .any(|f| f.http.is_none() && !f.needs_mock_server());
158
159        let needs_main_test = has_file_fixtures
160            || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
161                if f.needs_mock_server() {
162                    return true;
163                }
164                let cc = e2e_config.resolve_call_for_fixture(
165                    f.call.as_deref(),
166                    &f.id,
167                    &f.resolved_category(),
168                    &f.tags,
169                    &f.input,
170                );
171                let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
172                go_override.and_then(|o| o.client_factory.as_deref()).is_some()
173            });
174
175        if needs_main_test {
176            files.push(GeneratedFile {
177                path: output_base.join("main_test.go"),
178                content: render_main_test_go(&e2e_config.test_documents_dir),
179                generated_header: true,
180            });
181        }
182
183        // Generate test files per category.
184        for group in groups {
185            let active: Vec<&Fixture> = group
186                .fixtures
187                .iter()
188                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
189                .collect();
190
191            if active.is_empty() {
192                continue;
193            }
194
195            let filename = format!("{}_test.go", sanitize_filename(&group.category));
196            let content = render_test_file(
197                &group.category,
198                &active,
199                &module_path,
200                &import_alias,
201                e2e_config,
202                &config.adapters,
203            );
204            files.push(GeneratedFile {
205                path: output_base.join(filename),
206                content,
207                generated_header: true,
208            });
209        }
210
211        Ok(files)
212    }
213
214    fn language_name(&self) -> &'static str {
215        "go"
216    }
217}
218
219/// Fix a Go module version so it is valid for a major-version module path.
220///
221/// Go requires that a module path ending in `/vN` (N ≥ 2) uses a version
222/// whose major component matches N.  In local-replace mode we use a synthetic
223/// placeholder version; if that placeholder (e.g. `v0.0.0`) doesn't match the
224/// major suffix, fix it to `vN.0.0` so `go mod` accepts the go.mod.
225fn fix_go_major_version(module_path: &str, version: &str) -> String {
226    // Extract `/vN` suffix from the module path (N must be ≥ 2).
227    let major = module_path
228        .rsplit('/')
229        .next()
230        .and_then(|seg| seg.strip_prefix('v'))
231        .and_then(|n| n.parse::<u64>().ok())
232        .filter(|&n| n >= 2);
233
234    let Some(n) = major else {
235        return version.to_string();
236    };
237
238    // If the version already starts with `vN.`, it is valid — leave it alone.
239    let expected_prefix = format!("v{n}.");
240    if version.starts_with(&expected_prefix) {
241        return version.to_string();
242    }
243
244    format!("v{n}.0.0")
245}
246
247fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
248    let mut out = String::new();
249    let _ = writeln!(out, "module e2e_go");
250    let _ = writeln!(out);
251    let _ = writeln!(out, "go 1.26");
252    let _ = writeln!(out);
253    let _ = writeln!(out, "require (");
254    let _ = writeln!(out, "\t{go_module_path} {version}");
255    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
256    let _ = writeln!(out, ")");
257
258    if let Some(path) = replace_path {
259        let _ = writeln!(out);
260        let _ = writeln!(out, "replace {go_module_path} => {path}");
261    }
262
263    out
264}
265
266/// Generate `main_test.go` that starts the mock HTTP server before all tests run.
267///
268/// The binary is expected at `../rust/target/release/mock-server` relative to the Go e2e
269/// directory.  The server prints `MOCK_SERVER_URL=http://...` on stdout; we read that line
270/// and export the variable so all test files can call `os.Getenv("MOCK_SERVER_URL")`.
271fn render_main_test_go(test_documents_dir: &str) -> String {
272    // NOTE: the generated-file header is injected by the caller (generated_header: true).
273    let mut out = String::new();
274    let _ = writeln!(out, "package e2e_test");
275    let _ = writeln!(out);
276    let _ = writeln!(out, "import (");
277    let _ = writeln!(out, "\t\"bufio\"");
278    let _ = writeln!(out, "\t\"encoding/json\"");
279    let _ = writeln!(out, "\t\"io\"");
280    let _ = writeln!(out, "\t\"os\"");
281    let _ = writeln!(out, "\t\"os/exec\"");
282    let _ = writeln!(out, "\t\"path/filepath\"");
283    let _ = writeln!(out, "\t\"runtime\"");
284    let _ = writeln!(out, "\t\"strings\"");
285    let _ = writeln!(out, "\t\"testing\"");
286    let _ = writeln!(out, ")");
287    let _ = writeln!(out);
288    let _ = writeln!(out, "func TestMain(m *testing.M) {{");
289    let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
290    let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
291    let _ = writeln!(out);
292    let _ = writeln!(
293        out,
294        "\t// Change to the configured test-documents directory (if it exists) so that fixture"
295    );
296    let _ = writeln!(
297        out,
298        "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
299    );
300    let _ = writeln!(
301        out,
302        "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
303    );
304    let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
305    let _ = writeln!(
306        out,
307        "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
308    );
309    let _ = writeln!(
310        out,
311        "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
312    );
313    let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
314    let _ = writeln!(out, "\t\t\tpanic(err)");
315    let _ = writeln!(out, "\t\t}}");
316    let _ = writeln!(out, "\t}}");
317    let _ = writeln!(out);
318    let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
319    let _ = writeln!(
320        out,
321        "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
322    );
323    let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
324    let _ = writeln!(
325        out,
326        "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
327    );
328    let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
329    let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
330    let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
331    let _ = writeln!(out, "\t\tif err != nil {{");
332    let _ = writeln!(out, "\t\t\tpanic(err)");
333    let _ = writeln!(out, "\t\t}}");
334    let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
335    let _ = writeln!(
336        out,
337        "\t\t// server does not see EOF and exit immediately. The mock-server"
338    );
339    let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
340    let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
341    let _ = writeln!(out, "\t\tif err != nil {{");
342    let _ = writeln!(out, "\t\t\tpanic(err)");
343    let _ = writeln!(out, "\t\t}}");
344    let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
345    let _ = writeln!(out, "\t\t\tpanic(err)");
346    let _ = writeln!(out, "\t\t}}");
347    let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
348    let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
349    let _ = writeln!(out, "\t\t\tline := scanner.Text()");
350    let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
351    let _ = writeln!(
352        out,
353        "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
354    );
355    let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
356    let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
357    let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
358    let _ = writeln!(
359        out,
360        "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
361    );
362    let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
363    let _ = writeln!(
364        out,
365        "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
366    );
367    let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
368    let _ = writeln!(
369        out,
370        "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
371    );
372    let _ = writeln!(out, "\t\t\t\t\t}}");
373    let _ = writeln!(out, "\t\t\t\t}}");
374    let _ = writeln!(out, "\t\t\t\tbreak");
375    let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
376    let _ = writeln!(out, "\t\t\t\tbreak");
377    let _ = writeln!(out, "\t\t\t}}");
378    let _ = writeln!(out, "\t\t}}");
379    let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
380    let _ = writeln!(out, "\t\tcode := m.Run()");
381    let _ = writeln!(out, "\t\t_ = stdin.Close()");
382    let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
383    let _ = writeln!(out, "\t\t_ = cmd.Wait()");
384    let _ = writeln!(out, "\t\tos.Exit(code)");
385    let _ = writeln!(out, "\t}} else {{");
386    let _ = writeln!(out, "\t\tcode := m.Run()");
387    let _ = writeln!(out, "\t\tos.Exit(code)");
388    let _ = writeln!(out, "\t}}");
389    let _ = writeln!(out, "}}");
390    out
391}
392
393/// Generate `helpers_test.go` with the jsonString helper function.
394/// This is emitted once per package to avoid duplicate function definitions.
395fn render_helpers_test_go() -> String {
396    let mut out = String::new();
397    let _ = writeln!(out, "package e2e_test");
398    let _ = writeln!(out);
399    let _ = writeln!(out, "import \"encoding/json\"");
400    let _ = writeln!(out);
401    let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
402    let _ = writeln!(
403        out,
404        "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
405    );
406    let _ = writeln!(out, "func jsonString(value any) string {{");
407    let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
408    let _ = writeln!(out, "\tif err != nil {{");
409    let _ = writeln!(out, "\t\treturn \"\"");
410    let _ = writeln!(out, "\t}}");
411    let _ = writeln!(out, "\treturn string(encoded)");
412    let _ = writeln!(out, "}}");
413    out
414}
415
416fn render_test_file(
417    category: &str,
418    fixtures: &[&Fixture],
419    go_module_path: &str,
420    import_alias: &str,
421    e2e_config: &crate::config::E2eConfig,
422    adapters: &[alef_core::config::AdapterConfig],
423) -> String {
424    let mut out = String::new();
425    let emits_executable_test =
426        |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
427
428    // Go convention: generated file marker must appear before the package declaration.
429    out.push_str(&hash::header(CommentStyle::DoubleSlash));
430    let _ = writeln!(out);
431
432    // Determine if any fixture actually uses the pkg import.
433    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
434    // package — omit the import when no fixture needs it to avoid the Go "imported and not
435    // used" compile error. Visitor fixtures reference the package types (NodeContext,
436    // VisitResult, VisitResult* helpers) in struct method signatures emitted at file scope,
437    // so they also require the import even when the test body itself is a Skip stub.
438    // Direct-callable fixtures (non-HTTP, non-mock, with a resolved Go function) also
439    // reference the package when a Go override function is configured.
440    let needs_pkg = fixtures
441        .iter()
442        .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
443
444    // Determine if we need the "os" import (mock_url args, HTTP fixtures, or
445    // client_factory fixtures that read MOCK_SERVER_URL via os.Getenv).
446    let needs_os = fixtures.iter().any(|f| {
447        if f.is_http_test() {
448            return true;
449        }
450        if !emits_executable_test(f) {
451            return false;
452        }
453        let call_config =
454            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
455        let go_override = call_config
456            .overrides
457            .get("go")
458            .or_else(|| e2e_config.call.overrides.get("go"));
459        if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
460            return true;
461        }
462        let call_args = &call_config.args;
463        // Need "os" for mock_url / mock_url_list args, or for bytes args with a string
464        // fixture value (fixture-relative path loaded via os.ReadFile at test-run time).
465        if call_args
466            .iter()
467            .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
468        {
469            return true;
470        }
471        call_args.iter().any(|a| {
472            if a.arg_type != "bytes" {
473                return false;
474            }
475            // alef.toml field paths are dotted (e.g. "input.data"). The fixture's `input`
476            // field already strips the "input." prefix, so we walk the remaining segments.
477            let mut current = &f.input;
478            let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
479            for segment in path.split('.') {
480                match current.get(segment) {
481                    Some(next) => current = next,
482                    None => return false,
483                }
484            }
485            current.is_string()
486        })
487    });
488
489    // Note: file_path args are passed directly as relative strings — the e2e/go
490    // TestMain in main_test.go already chdir's into the repo-root test_documents/.
491    let needs_filepath = false;
492
493    let _needs_json_stringify = fixtures.iter().any(|f| {
494        emits_executable_test(f)
495            && f.assertions.iter().any(|a| {
496                matches!(
497                    a.assertion_type.as_str(),
498                    "contains" | "contains_all" | "contains_any" | "not_contains"
499                ) && {
500                    // Check if this assertion operates on an array field.
501                    // If no field is specified, check if the result itself is an array.
502                    if a.field.as_ref().is_none_or(|f| f.is_empty()) {
503                        // No field specified: check if result is an array
504                        e2e_config
505                            .resolve_call_for_fixture(
506                                f.call.as_deref(),
507                                &f.id,
508                                &f.resolved_category(),
509                                &f.tags,
510                                &f.input,
511                            )
512                            .result_is_array
513                    } else {
514                        // Field specified: check if that field is an array
515                        let cc = e2e_config.resolve_call_for_fixture(
516                            f.call.as_deref(),
517                            &f.id,
518                            &f.resolved_category(),
519                            &f.tags,
520                            &f.input,
521                        );
522                        let per_call_resolver = FieldResolver::new(
523                            e2e_config.effective_fields(cc),
524                            e2e_config.effective_fields_optional(cc),
525                            e2e_config.effective_result_fields(cc),
526                            e2e_config.effective_fields_array(cc),
527                            &std::collections::HashSet::new(),
528                        );
529                        let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
530                        per_call_resolver.is_array(resolved_name)
531                    }
532                }
533            })
534    });
535
536    // Determine if we need "encoding/json" (handle args with non-null config,
537    // json_object args that will be unmarshalled into a typed struct, or HTTP
538    // body/partial/validation-error assertions that use json.Unmarshal).
539    let needs_json = fixtures.iter().any(|f| {
540        // HTTP body assertions use json.Unmarshal for Object/Array bodies;
541        // partial body and validation-error assertions always use json.Unmarshal.
542        if let Some(http) = &f.http {
543            let body_needs_json = http
544                .expected_response
545                .body
546                .as_ref()
547                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
548            let partial_needs_json = http.expected_response.body_partial.is_some();
549            let ve_needs_json = http
550                .expected_response
551                .validation_errors
552                .as_ref()
553                .is_some_and(|v| !v.is_empty());
554            if body_needs_json || partial_needs_json || ve_needs_json {
555                return true;
556            }
557        }
558        if !emits_executable_test(f) {
559            return false;
560        }
561
562        let call =
563            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
564        let call_args = &call.args;
565        // handle args with non-null config value
566        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
567            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
568                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
569                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
570                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
571            })
572        };
573        // json_object args with options_type or array values (will use JSON unmarshal)
574        let go_override = call.overrides.get("go");
575        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
576            e2e_config
577                .call
578                .overrides
579                .get("go")
580                .and_then(|o| o.options_type.as_deref())
581        });
582        let has_json_obj = call_args.iter().any(|a| {
583            if a.arg_type != "json_object" {
584                return false;
585            }
586            let v = if a.field == "input" {
587                &f.input
588            } else {
589                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
590                f.input.get(field).unwrap_or(&serde_json::Value::Null)
591            };
592            if v.is_array() {
593                return true;
594            } // array → []string unmarshal
595            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
596        });
597        has_handle || has_json_obj
598    });
599
600    // No runtime base64 calls remain in generated Go code. Bytes args with string values
601    // are now loaded via os.ReadFile (see needs_os) and HTTP body byte arrays are
602    // base64-encoded at codegen time and embedded as literal strings in the json.Unmarshal
603    // call, which doesn't require the `encoding/base64` import in the test file.
604    let needs_base64 = false;
605
606    // Determine if we need the "fmt" import (CustomTemplate visitor actions
607    // with placeholders, string assertions rendered through fmt.Sprint, or
608    // optional field variable bindings using fmt.Sprintf).
609    // Note: jsonString is now in helpers_test.go (uses encoding/json, not fmt),
610    // so individual test files do NOT need fmt just for calling jsonString.
611    let needs_fmt = fixtures.iter().any(|f| {
612        // Check CustomTemplate visitor actions
613        if f.visitor.as_ref().is_some_and(|v| {
614            v.callbacks.values().any(|action| {
615                if let CallbackAction::CustomTemplate { template, .. } = action {
616                    template.contains('{')
617                } else {
618                    false
619                }
620            })
621        }) {
622            return true;
623        }
624
625        if !emits_executable_test(f) {
626            return false;
627        }
628
629        // Check if any assertion uses fmt.Sprint (contains assertions with non-array fields)
630        if f.assertions.iter().any(|a| {
631            matches!(
632                a.assertion_type.as_str(),
633                "contains" | "contains_all" | "contains_any" | "not_contains"
634            ) && {
635                if a.field.as_ref().is_none_or(|f| f.is_empty()) {
636                    // No field: fmt.Sprint only if result is not an array
637                    !e2e_config
638                        .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
639                        .result_is_array
640                } else {
641                    // Field specified: fmt.Sprint only if that field is not an array
642                    // and the field is actually valid for the result type.
643                    let field = a.field.as_deref().unwrap_or("");
644                    let cc = e2e_config.resolve_call_for_fixture(
645                        f.call.as_deref(),
646                        &f.id,
647                        &f.resolved_category(),
648                        &f.tags,
649                        &f.input,
650                    );
651                    let per_call_resolver = FieldResolver::new(
652                        e2e_config.effective_fields(cc),
653                        e2e_config.effective_fields_optional(cc),
654                        e2e_config.effective_result_fields(cc),
655                        e2e_config.effective_fields_array(cc),
656                        &std::collections::HashSet::new(),
657                    );
658                    let resolved_name = per_call_resolver.resolve(field);
659                    !per_call_resolver.is_array(resolved_name) && per_call_resolver.is_valid_for_result(field)
660                }
661            }
662        }) {
663            return true;
664        }
665
666        // Check if any assertion has an optional string field (triggers fmt.Sprintf in variable binding)
667        f.assertions.iter().any(|a| {
668            if let Some(field) = &a.field {
669                if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
670                    let cc = e2e_config.resolve_call_for_fixture(
671                        f.call.as_deref(),
672                        &f.id,
673                        &f.resolved_category(),
674                        &f.tags,
675                        &f.input,
676                    );
677                    let per_call_resolver = FieldResolver::new(
678                        e2e_config.effective_fields(cc),
679                        e2e_config.effective_fields_optional(cc),
680                        e2e_config.effective_result_fields(cc),
681                        e2e_config.effective_fields_array(cc),
682                        &std::collections::HashSet::new(),
683                    );
684                    let resolved = per_call_resolver.resolve(field);
685                    // Need fmt if field is optional, not an array, not a map, and valid for result
686                    per_call_resolver.is_optional(resolved)
687                        && !per_call_resolver.is_array(resolved)
688                        && !per_call_resolver.has_map_access(field)
689                        && per_call_resolver.is_valid_for_result(field)
690                } else {
691                    false
692                }
693            } else {
694                false
695            }
696        })
697    });
698
699    // Determine if we need the "strings" import.
700    // Only count assertions whose fields are actually valid for the result type.
701    // Also check if any fixture has mock_url_list args (needs strings.HasPrefix).
702    let needs_strings = fixtures.iter().any(|f| {
703        if !emits_executable_test(f) {
704            return false;
705        }
706        // Check for mock_url_list in call args
707        let cc =
708            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
709        if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
710            return true;
711        }
712        let per_call_resolver = FieldResolver::new(
713            e2e_config.effective_fields(cc),
714            e2e_config.effective_fields_optional(cc),
715            e2e_config.effective_result_fields(cc),
716            e2e_config.effective_fields_array(cc),
717            &std::collections::HashSet::new(),
718        );
719        f.assertions.iter().any(|a| {
720            let type_needs_strings = if a.assertion_type == "equals" {
721                // equals with string values needs strings.TrimSpace
722                a.value.as_ref().is_some_and(|v| v.is_string())
723            } else {
724                matches!(
725                    a.assertion_type.as_str(),
726                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
727                )
728            };
729            let field_valid = a
730                .field
731                .as_ref()
732                .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
733                .unwrap_or(true);
734            type_needs_strings && field_valid
735        })
736    });
737
738    // Determine if we need the testify assert import.
739    let needs_assert = fixtures.iter().any(|f| {
740        if !emits_executable_test(f) {
741            return false;
742        }
743        // Validation-category fixtures with an `error` assertion emit
744        // `assert.Error(t, createErr)` in their setup block, requiring testify.
745        // Other categories (e.g. `error`) use t.Errorf/t.Fatalf and do NOT need testify.
746        if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
747            return true;
748        }
749        // Streaming fixtures emit `assert.X` calls against the collected `chunks`
750        // list — the field path resolves against the streaming virtual-field
751        // accessor table, not the result type. Treat any streaming-virtual field
752        // reference as `field_valid`.
753        let is_streaming_fixture = f.is_streaming_mock();
754        let cc =
755            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
756        let per_call_resolver = FieldResolver::new(
757            e2e_config.effective_fields(cc),
758            e2e_config.effective_fields_optional(cc),
759            e2e_config.effective_result_fields(cc),
760            e2e_config.effective_fields_array(cc),
761            &std::collections::HashSet::new(),
762        );
763        f.assertions.iter().any(|a| {
764            let field_is_streaming_virtual = a
765                .field
766                .as_deref()
767                .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
768            let field_valid = a
769                .field
770                .as_ref()
771                .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
772                .unwrap_or(true)
773                || (is_streaming_fixture && field_is_streaming_virtual);
774            let synthetic_field_needs_assert = match a.field.as_deref() {
775                Some(
776                    "chunks_have_content"
777                    | "chunks_have_embeddings"
778                    | "chunks_have_heading_context"
779                    | "first_chunk_starts_with_heading",
780                ) => {
781                    matches!(a.assertion_type.as_str(), "is_true" | "is_false")
782                }
783                Some("embeddings") => {
784                    matches!(
785                        a.assertion_type.as_str(),
786                        "count_equals" | "count_min" | "not_empty" | "is_empty"
787                    )
788                }
789                _ => false,
790            };
791            let type_needs_assert = matches!(
792                a.assertion_type.as_str(),
793                "count_equals"
794                    | "count_min"
795                    | "count_max"
796                    | "is_true"
797                    | "is_false"
798                    | "method_result"
799                    | "min_length"
800                    | "max_length"
801                    | "matches_regex"
802                    | "greater_than"
803                    | "greater_than_or_equal"
804                    | "less_than"
805                    | "less_than_or_equal"
806            );
807            synthetic_field_needs_assert || (type_needs_assert && (field_valid || field_is_streaming_virtual))
808        })
809    });
810
811    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
812    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
813    let needs_http = has_http_fixtures;
814    // io.ReadAll is emitted for every HTTP fixture (render_call always reads the body).
815    let needs_io = has_http_fixtures;
816
817    // Determine if we need "reflect" (for HTTP response body JSON comparison
818    // and partial-body assertions, both of which use reflect.DeepEqual).
819    let needs_reflect = fixtures.iter().any(|f| {
820        if let Some(http) = &f.http {
821            let body_needs_reflect = http
822                .expected_response
823                .body
824                .as_ref()
825                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
826            let partial_needs_reflect = http.expected_response.body_partial.is_some();
827            body_needs_reflect || partial_needs_reflect
828        } else {
829            false
830        }
831    });
832
833    // Render the file body (visitor structs + test functions) into a separate buffer
834    // first so we can scan the actual emitted code for `assert.` usage. This avoids
835    // false positives in the heuristic-based needs_assert detection: imports are emitted
836    // only when the rendered code actually uses them.
837    let mut body = String::new();
838    for fixture in fixtures.iter() {
839        if let Some(visitor_spec) = &fixture.visitor {
840            let struct_name = visitor_struct_name(&fixture.id);
841            emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
842            let _ = writeln!(body);
843        }
844    }
845    for (i, fixture) in fixtures.iter().enumerate() {
846        render_test_function(&mut body, fixture, import_alias, e2e_config, adapters);
847        if i + 1 < fixtures.len() {
848            let _ = writeln!(body);
849        }
850    }
851    let needs_assert = needs_assert || body.contains("assert.");
852
853    let _ = writeln!(out, "// E2e tests for category: {category}");
854    let _ = writeln!(out, "package e2e_test");
855    let _ = writeln!(out);
856    let _ = writeln!(out, "import (");
857    if needs_base64 {
858        let _ = writeln!(out, "\t\"encoding/base64\"");
859    }
860    if needs_json || needs_reflect {
861        let _ = writeln!(out, "\t\"encoding/json\"");
862    }
863    if needs_fmt {
864        let _ = writeln!(out, "\t\"fmt\"");
865    }
866    if needs_io {
867        let _ = writeln!(out, "\t\"io\"");
868    }
869    if needs_http {
870        let _ = writeln!(out, "\t\"net/http\"");
871    }
872    if needs_os {
873        let _ = writeln!(out, "\t\"os\"");
874    }
875    let _ = needs_filepath; // reserved for future use; currently always false
876    if needs_reflect {
877        let _ = writeln!(out, "\t\"reflect\"");
878    }
879    if needs_strings {
880        let _ = writeln!(out, "\t\"strings\"");
881    }
882    let _ = writeln!(out, "\t\"testing\"");
883    if needs_assert {
884        let _ = writeln!(out);
885        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
886    }
887
888    if needs_pkg {
889        let _ = writeln!(out);
890        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
891    }
892    let _ = writeln!(out, ")");
893    let _ = writeln!(out);
894
895    // Append the pre-rendered body (visitor structs + test functions).
896    out.push_str(&body);
897
898    // Clean up trailing newlines.
899    while out.ends_with("\n\n") {
900        out.pop();
901    }
902    if !out.ends_with('\n') {
903        out.push('\n');
904    }
905    out
906}
907
908/// Return `true` when a non-HTTP fixture can be exercised by calling the Go
909/// binding directly.
910///
911/// A fixture is Go-callable when the resolved call config provides a non-empty
912/// function name — either via a Go-specific override (`[e2e.call.overrides.go]
913/// function`) or via the base call `function` field.  The Go binding exposes all
914/// public functions from the Rust core as PascalCase exports, so any non-empty
915/// function name can be resolved to a valid Go symbol via `to_go_name`.
916fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
917    // HTTP fixtures are handled by render_http_test_function — not our concern here.
918    if fixture.is_http_test() {
919        return false;
920    }
921    let call_config = e2e_config.resolve_call_for_fixture(
922        fixture.call.as_deref(),
923        &fixture.id,
924        &fixture.resolved_category(),
925        &fixture.tags,
926        &fixture.input,
927    );
928    // Honor per-call `skip_languages`: when the resolved call's `skip_languages`
929    // contains `"go"`, the Go binding doesn't expose this function.
930    if call_config.skip_languages.iter().any(|l| l == "go") {
931        return false;
932    }
933    let go_override = call_config
934        .overrides
935        .get("go")
936        .or_else(|| e2e_config.call.overrides.get("go"));
937    // When a client_factory is configured, the fixture is callable via the
938    // client-method pattern even when the base function name is empty.
939    if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
940        return true;
941    }
942    // Prefer a Go-specific override function name; fall back to the base function name.
943    // Any non-empty function name is callable: the Go binding exports all public
944    // Rust functions as PascalCase symbols (snake_case → PascalCase via to_go_name).
945    let fn_name = go_override
946        .and_then(|o| o.function.as_deref())
947        .filter(|s| !s.is_empty())
948        .unwrap_or(call_config.function.as_str());
949    !fn_name.is_empty()
950}
951
952fn render_test_function(
953    out: &mut String,
954    fixture: &Fixture,
955    import_alias: &str,
956    e2e_config: &crate::config::E2eConfig,
957    adapters: &[alef_core::config::AdapterConfig],
958) {
959    let fn_name = fixture.id.to_upper_camel_case();
960    let description = &fixture.description;
961
962    // Delegate HTTP fixtures to the shared driver via GoTestClientRenderer.
963    if fixture.http.is_some() {
964        render_http_test_function(out, fixture);
965        return;
966    }
967
968    // Non-HTTP fixtures are tested directly when the call config provides a
969    // callable Go function.  Emit a t.Skip() stub when:
970    //   - No mock response and no callable (non-HTTP, non-mock, unreachable), or
971    //   - The call's skip_languages includes "go" (e.g. streaming not supported).
972    if !fixture_has_go_callable(fixture, e2e_config) {
973        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
974        let _ = writeln!(out, "\t// {description}");
975        let _ = writeln!(
976            out,
977            "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
978        );
979        let _ = writeln!(out, "}}");
980        return;
981    }
982
983    // Resolve call config per-fixture (supports named calls via fixture.call).
984    let call_config = e2e_config.resolve_call_for_fixture(
985        fixture.call.as_deref(),
986        &fixture.id,
987        &fixture.resolved_category(),
988        &fixture.tags,
989        &fixture.input,
990    );
991    // Per-call field resolver: uses effective fields/result_fields from the resolved call.
992    let call_field_resolver = FieldResolver::new(
993        e2e_config.effective_fields(call_config),
994        e2e_config.effective_fields_optional(call_config),
995        e2e_config.effective_result_fields(call_config),
996        e2e_config.effective_fields_array(call_config),
997        &std::collections::HashSet::new(),
998    );
999    let field_resolver = &call_field_resolver;
1000    let lang = "go";
1001    let overrides = call_config.overrides.get(lang);
1002
1003    // Select the function name: Go bindings now integrate visitor support into
1004    // the main Convert() function via ConversionOptions.Visitor field.
1005    // (In other languages, there may be separate visitor_function overrides, but Go uses a single function.)
1006    let base_function_name = overrides
1007        .and_then(|o| o.function.as_deref())
1008        .unwrap_or(&call_config.function);
1009    let function_name = to_go_name(base_function_name);
1010    let result_var = &call_config.result_var;
1011    let args = &call_config.args;
1012
1013    // Whether the function returns (value, error) or just (error) or just (value).
1014    // Check Go override first, fall back to call-level returns_result.
1015    let returns_result = overrides
1016        .and_then(|o| o.returns_result)
1017        .unwrap_or(call_config.returns_result);
1018
1019    // Whether the function returns only error (no value component), i.e. Result<(), E>.
1020    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
1021    let returns_void = call_config.returns_void;
1022
1023    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
1024    // Boolean OR with call-level — serde defaults `result_is_simple` to `false` on
1025    // CallOverride, so a Go override that only sets e.g. `returns_result = false`
1026    // would otherwise silently clobber a true call-level value. Falls back to the
1027    // rust override only when neither the Go override nor the call-level value is set.
1028    let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
1029        || call_config.result_is_simple
1030        || call_config
1031            .overrides
1032            .get("rust")
1033            .map(|o| o.result_is_simple)
1034            .unwrap_or(false);
1035
1036    // result_is_array: the simple result is a slice/array type (e.g., []string).
1037    // Boolean OR with call-level — serde defaults `result_is_array` to `false` on
1038    // CallOverride, so a Go override that only sets `result_is_pointer` would
1039    // otherwise silently mask a true call-level value. Until the field becomes
1040    // Option<bool>, OR'ing is the only safe coalesce.
1041    let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
1042
1043    // Per-call Go options_type, falling back to the default call's Go override.
1044    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
1045        e2e_config
1046            .call
1047            .overrides
1048            .get("go")
1049            .and_then(|o| o.options_type.as_deref())
1050    });
1051
1052    // Whether json_object options are passed as a pointer (*OptionsType).
1053    let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1054        e2e_config
1055            .call
1056            .overrides
1057            .get("go")
1058            .map(|o| o.options_ptr)
1059            .unwrap_or(false)
1060    });
1061
1062    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1063    // Validation-category fixtures expect engine *creation* to fail. Other expects_error
1064    // fixtures (error_*) construct a valid engine and expect the *operation* to fail —
1065    // engine creation should not be wrapped in assert.Error there.
1066    let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1067
1068    // Client factory: when set, the test creates a client via `pkg.Factory("test-key", baseURL)`
1069    // and calls methods on the instance rather than top-level package functions.
1070    let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1071        e2e_config
1072            .call
1073            .overrides
1074            .get(lang)
1075            .and_then(|o| o.client_factory.as_deref())
1076    });
1077
1078    let (mut setup_lines, args_str) = build_args_and_setup(
1079        &fixture.input,
1080        args,
1081        import_alias,
1082        call_options_type,
1083        fixture,
1084        call_options_ptr,
1085        validation_creation_failure,
1086    );
1087
1088    // Build visitor if present — integrate into options instead of separate parameter.
1089    // Go binding's Convert() checks options.Visitor and delegates to convertWithVisitorHelper when set.
1090    let mut visitor_opts_var: Option<String> = None;
1091    if fixture.visitor.is_some() {
1092        let struct_name = visitor_struct_name(&fixture.id);
1093        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1094        // Create a fresh opts variable with the visitor attached.
1095        let opts_type = call_options_type.unwrap_or("ConversionOptions");
1096        let opts_var = "opts".to_string();
1097        setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1098        setup_lines.push("opts.Visitor = visitor".to_string());
1099        visitor_opts_var = Some(opts_var);
1100    }
1101
1102    let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1103    let final_args = {
1104        let mut parts: Vec<String> = Vec::new();
1105        if !args_str.is_empty() {
1106            // When visitor is present, replace trailing ", nil" with ", opts"
1107            let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1108                args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1109            } else {
1110                args_str
1111            };
1112            parts.push(processed_args);
1113        }
1114        parts.extend(go_extra_args);
1115        parts.join(", ")
1116    };
1117
1118    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1119    let _ = writeln!(out, "\t// {description}");
1120
1121    // Live-API fixtures use `env.api_key_var` to mark the env var that
1122    // supplies the real API key. Skip the test when the env var is unset
1123    // (mirrors Python's pytest.skip and Node's early-return pattern).
1124    let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1125    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1126    if let Some(var) = api_key_var {
1127        if has_mock {
1128            // Env-fallback branch: when the real API key is set use the live
1129            // provider; otherwise fall back to the mock server so the test
1130            // always runs in CI without credentials.
1131            let fixture_id = &fixture.id;
1132            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1133            let _ = writeln!(out, "\tvar baseURL *string");
1134            let _ = writeln!(out, "\tif apiKey != \"\" {{");
1135            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1136            let _ = writeln!(out, "\t}} else {{");
1137            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1138            let _ = writeln!(
1139                out,
1140                "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1141            );
1142            let _ = writeln!(out, "\t\tbaseURL = &u");
1143            let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1144            let _ = writeln!(out, "\t}}");
1145        } else {
1146            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1147            let _ = writeln!(out, "\tif apiKey == \"\" {{");
1148            let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1149            let _ = writeln!(out, "\t}}");
1150        }
1151    }
1152
1153    for line in &setup_lines {
1154        let _ = writeln!(out, "\t{line}");
1155    }
1156
1157    // Client factory: emit client creation before the call.
1158    // Each test creates a fresh client pointed at MOCK_SERVER_URL/fixtures/<id>
1159    // so the mock server can serve the fixture response via prefix routing.
1160    let call_prefix = if let Some(factory) = client_factory {
1161        let factory_name = to_go_name(factory);
1162        let fixture_id = &fixture.id;
1163        // Determine how to express the API key and base URL for the client
1164        // constructor call, depending on which code path was emitted above.
1165        let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1166            // Env-fallback: local vars emitted above carry the right values.
1167            ("apiKey".to_string(), "baseURL".to_string())
1168        } else if api_key_var.is_some() {
1169            // Skip-unless-set: live API only, no mock fallback.
1170            ("apiKey".to_string(), "nil".to_string())
1171        } else if fixture.has_host_root_route() {
1172            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1173            let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1174            let _ = writeln!(out, "\tif mockURL == \"\" {{");
1175            let _ = writeln!(
1176                out,
1177                "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1178            );
1179            let _ = writeln!(out, "\t}}");
1180            ("\"test-key\"".to_string(), "&mockURL".to_string())
1181        } else {
1182            let _ = writeln!(
1183                out,
1184                "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1185            );
1186            ("\"test-key\"".to_string(), "&mockURL".to_string())
1187        };
1188        let _ = writeln!(
1189            out,
1190            "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1191        );
1192        let _ = writeln!(out, "\tif clientErr != nil {{");
1193        let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1194        let _ = writeln!(out, "\t}}");
1195        "client".to_string()
1196    } else {
1197        import_alias.to_string()
1198    };
1199
1200    // The Go binding generator wraps the FFI call in `(T, error)` whenever any
1201    // param requires JSON marshalling, even when the underlying Rust function
1202    // does not return Result. Detect that so error-expecting tests emit `_, err :=`
1203    // instead of `err :=` when the binding has a value component.
1204    let binding_returns_error_pre = args
1205        .iter()
1206        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1207    let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1208
1209    if expects_error {
1210        if effective_returns_result_pre && !returns_void {
1211            let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1212        } else {
1213            let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1214        }
1215        let _ = writeln!(out, "\tif err == nil {{");
1216        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1217        let _ = writeln!(out, "\t}}");
1218        let _ = writeln!(out, "}}");
1219        return;
1220    }
1221
1222    // Detect streaming fixtures (call-level `streaming` opt-out is honored).
1223    let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1224
1225    // Determine the streaming item type for this call (used when draining the channel).
1226    // Find the first streaming adapter matching the function name (e.g., crawl_stream → CrawlEvent).
1227    // Match on snake_case adapter name vs. snake_case function name; e2e codegen sees
1228    // camelCase function names like `CrawlStream` but adapters declare `crawl_stream`.
1229    use heck::ToSnakeCase;
1230    let fn_snake = function_name.to_snake_case();
1231    let base_snake = base_function_name.to_snake_case();
1232    let streaming_item_type = if is_streaming {
1233        adapters
1234            .iter()
1235            .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1236            .find(|a| a.name == fn_snake || a.name == base_snake)
1237            .and_then(|a| a.item_type.as_deref())
1238            .and_then(|t| t.rsplit("::").next())
1239            .unwrap_or("Item") // Fallback if no matching adapter is declared
1240    } else {
1241        "Item" // Unused, but needed for type consistency
1242    };
1243
1244    // Check if any assertion actually uses the result variable.
1245    // If all assertions are skipped (field not on result type), use `_` to avoid
1246    // Go's "declared and not used" compile error.
1247    // For streaming fixtures: streaming virtual fields count as usable.
1248    let has_usable_assertion = fixture.assertions.iter().any(|a| {
1249        if a.assertion_type == "not_error" || a.assertion_type == "error" {
1250            return false;
1251        }
1252        // method_result assertions always use the result variable.
1253        if a.assertion_type == "method_result" {
1254            return true;
1255        }
1256        match &a.field {
1257            Some(f) if !f.is_empty() => {
1258                if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1259                    return true;
1260                }
1261                field_resolver.is_valid_for_result(f)
1262            }
1263            _ => true,
1264        }
1265    });
1266
1267    // The Go binding generator (alef-backend-go) wraps the FFI call in `(T, error)`
1268    // whenever any param requires JSON marshalling (Vec, Map, Named struct), even when
1269    // the underlying Rust function does not return Result. So a result_is_simple call
1270    // like `generate_cache_key(parts: &[(String, String)]) -> String` still surfaces in
1271    // Go as `func GenerateCacheKey(parts [][]string) (*string, error)`. Detect that
1272    // here so the test emits `_, err :=` / `result, err :=` instead of `result :=`.
1273    let binding_returns_error = args
1274        .iter()
1275        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1276    // Client-factory methods always return (value, error) in the Go binding.
1277    let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1278
1279    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
1280    // We create a local `value` that dereferences it so assertions can use a plain type.
1281    // For functions that return (value, error): emit `result, err :=`
1282    // For functions that return only error: emit `err :=`
1283    // For functions that return only a value (result_is_simple, no error): emit `result :=`
1284    if !effective_returns_result && result_is_simple {
1285        // Function returns a single value, no error (e.g. *string, *bool).
1286        let result_binding = if has_usable_assertion {
1287            result_var.to_string()
1288        } else {
1289            "_".to_string()
1290        };
1291        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
1292        let assign_op = if result_binding == "_" { "=" } else { ":=" };
1293        let _ = writeln!(
1294            out,
1295            "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1296        );
1297        if has_usable_assertion && result_binding != "_" {
1298            if result_is_array {
1299                // Array results are slices (not pointers); assign directly without dereference.
1300                let _ = writeln!(out, "\tvalue := {result_var}");
1301            } else {
1302                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1303                // If so, skip dereference — we'll use the pointer directly.
1304                let only_nil_assertions = fixture
1305                    .assertions
1306                    .iter()
1307                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1308                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1309                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1310
1311                if !only_nil_assertions {
1312                    // Emit nil check and dereference for simple pointer results only if
1313                    // the result is actually a pointer (determined by result_is_pointer override).
1314                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1315                    if result_is_ptr {
1316                        let _ = writeln!(out, "\tif {result_var} == nil {{");
1317                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1318                        let _ = writeln!(out, "\t}}");
1319                        let _ = writeln!(out, "\tvalue := *{result_var}");
1320                    } else {
1321                        // Result is a value type (not a pointer), use directly without dereference.
1322                        let _ = writeln!(out, "\tvalue := {result_var}");
1323                    }
1324                }
1325            }
1326        }
1327    } else if !effective_returns_result || returns_void {
1328        // Function returns only error (either returns_result=false, or returns_result=true
1329        // with returns_void=true meaning the Go function signature is `func(...) error`).
1330        let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1331        let _ = writeln!(out, "\tif err != nil {{");
1332        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1333        let _ = writeln!(out, "\t}}");
1334        // No result variable to use in assertions.
1335        let _ = writeln!(out, "}}");
1336        return;
1337    } else {
1338        // returns_result = true, returns_void = false: function returns (value, error).
1339        // For streaming fixtures, always capture the channel as `stream`.
1340        let result_binding = if is_streaming {
1341            "stream".to_string()
1342        } else if has_usable_assertion {
1343            result_var.to_string()
1344        } else {
1345            "_".to_string()
1346        };
1347        let _ = writeln!(
1348            out,
1349            "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1350        );
1351        let _ = writeln!(out, "\tif err != nil {{");
1352        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1353        let _ = writeln!(out, "\t}}");
1354        // For streaming fixtures: drain the channel into a []T slice.
1355        if is_streaming {
1356            let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1357            let _ = writeln!(out, "\tfor chunk := range stream {{");
1358            let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1359            let _ = writeln!(out, "\t}}");
1360        }
1361        if result_is_simple && has_usable_assertion && result_binding != "_" {
1362            if result_is_array {
1363                // Array results are slices (not pointers); assign directly without dereference.
1364                let _ = writeln!(out, "\tvalue := {}", result_var);
1365            } else {
1366                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1367                // If so, skip dereference — we'll use the pointer directly.
1368                let only_nil_assertions = fixture
1369                    .assertions
1370                    .iter()
1371                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1372                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1373                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1374
1375                if !only_nil_assertions {
1376                    // Emit nil check and dereference for simple pointer results only if
1377                    // the result is actually a pointer (determined by result_is_pointer override).
1378                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1379                    if result_is_ptr {
1380                        let _ = writeln!(out, "\tif {} == nil {{", result_var);
1381                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1382                        let _ = writeln!(out, "\t}}");
1383                        let _ = writeln!(out, "\tvalue := *{}", result_var);
1384                    } else {
1385                        // Result is a value type (not a pointer), use directly without dereference.
1386                        let _ = writeln!(out, "\tvalue := {}", result_var);
1387                    }
1388                }
1389            }
1390        }
1391    }
1392
1393    // For result_is_simple functions, determine if we created a dereferenced `value` variable.
1394    // We skip dereferencing if all simple-result assertions are is_empty/is_null with no field,
1395    // or if the result is a value type (not a pointer).
1396    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1397    let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1398        let only_nil_assertions = fixture
1399            .assertions
1400            .iter()
1401            .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1402            .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1403            .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1404        !only_nil_assertions
1405    } else if result_is_simple && has_usable_assertion && result_is_ptr {
1406        true
1407    } else {
1408        result_is_simple && has_usable_assertion
1409    };
1410
1411    let effective_result_var = if has_deref_value {
1412        "value".to_string()
1413    } else {
1414        result_var.to_string()
1415    };
1416
1417    // Collect optional fields referenced by assertions and emit nil-safe
1418    // dereference blocks so that assertions can use plain string locals.
1419    // Only dereference fields whose assertion values are strings (or that are
1420    // used in string-oriented assertions like equals/contains with string values).
1421    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1422    for assertion in &fixture.assertions {
1423        if let Some(f) = &assertion.field {
1424            if !f.is_empty() {
1425                let resolved = field_resolver.resolve(f);
1426                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1427                    // Only create deref locals for string-valued fields that are NOT arrays.
1428                    // Array fields (e.g., *[]string) must keep their pointer form so
1429                    // render_assertion can emit strings.Join(*field, " ") rather than
1430                    // treating them as plain strings.
1431                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1432                    let is_array_field = field_resolver.is_array(resolved);
1433                    if !is_string_field || is_array_field {
1434                        // Non-string optional fields (e.g., *uint64) and array optional
1435                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
1436                        continue;
1437                    }
1438                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1439                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1440                    if field_resolver.has_map_access(f) {
1441                        // Go map access returns a value type (string), not a pointer.
1442                        // Use the value directly — empty string means not present.
1443                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
1444                    } else {
1445                        let _ = writeln!(out, "\tvar {local_var} string");
1446                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
1447                        // Use fmt.Sprintf to safely convert any type (named string types, structs, etc.)
1448                        // to string. This handles both *FinishReason (named type) and *FormatMetadata
1449                        // (struct types) without compile errors.
1450                        let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1451                        let _ = writeln!(out, "\t}}");
1452                    }
1453                    optional_locals.insert(f.clone(), local_var);
1454                }
1455            }
1456        }
1457    }
1458
1459    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
1460    for assertion in &fixture.assertions {
1461        if let Some(f) = &assertion.field {
1462            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1463                // Check if any prefix of the dotted path is optional (pointer in Go).
1464                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
1465                let parts: Vec<&str> = f.split('.').collect();
1466                let mut guard_expr: Option<String> = None;
1467                for i in 1..parts.len() {
1468                    let prefix = parts[..i].join(".");
1469                    let resolved_prefix = field_resolver.resolve(&prefix);
1470                    if field_resolver.is_optional(resolved_prefix) {
1471                        // If the prefix ends with a numeric index (e.g. "segments[0]"),
1472                        // the element itself is a value type in Go — it cannot be nil.
1473                        // Use the array field without the index (e.g. "segments") as the
1474                        // nil guard instead, so we emit `result.Segments != nil` rather
1475                        // than the invalid `result.Segments[0] != nil`.
1476                        let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1477                            let suffix = &resolved_prefix[bracket_pos + 1..];
1478                            let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1479                            if is_numeric_index {
1480                                &resolved_prefix[..bracket_pos]
1481                            } else {
1482                                resolved_prefix
1483                            }
1484                        } else {
1485                            resolved_prefix
1486                        };
1487                        let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1488                        guard_expr = Some(accessor);
1489                        break;
1490                    }
1491                }
1492                if let Some(guard) = guard_expr {
1493                    // Only emit nil guard if the assertion will actually produce code
1494                    // (not just a skip comment), to avoid empty branches (SA9003).
1495                    if field_resolver.is_valid_for_result(f) {
1496                        // For Go, avoid emitting nil checks on struct value types.
1497                        // In Go, struct types that are not wrapped in Option<T> in Rust
1498                        // remain value types in the Go binding, so they cannot be nil.
1499                        // Skip the guard if the expression is a direct struct field access
1500                        // (no array indexing, no map keys, no function calls).
1501                        let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1502                        if is_struct_value {
1503                            // Guard refers to a struct value type — skip the nil check
1504                            // and render the assertion directly.
1505                            render_assertion(
1506                                out,
1507                                assertion,
1508                                &effective_result_var,
1509                                import_alias,
1510                                field_resolver,
1511                                &optional_locals,
1512                                result_is_simple,
1513                                result_is_array,
1514                                is_streaming,
1515                            );
1516                            continue;
1517                        }
1518                        let _ = writeln!(out, "\tif {guard} != nil {{");
1519                        // Render into a temporary buffer so we can re-indent by one
1520                        // tab level to sit inside the nil-guard block.
1521                        let mut nil_buf = String::new();
1522                        render_assertion(
1523                            &mut nil_buf,
1524                            assertion,
1525                            &effective_result_var,
1526                            import_alias,
1527                            field_resolver,
1528                            &optional_locals,
1529                            result_is_simple,
1530                            result_is_array,
1531                            is_streaming,
1532                        );
1533                        for line in nil_buf.lines() {
1534                            let _ = writeln!(out, "\t{line}");
1535                        }
1536                        let _ = writeln!(out, "\t}}");
1537                    } else {
1538                        render_assertion(
1539                            out,
1540                            assertion,
1541                            &effective_result_var,
1542                            import_alias,
1543                            field_resolver,
1544                            &optional_locals,
1545                            result_is_simple,
1546                            result_is_array,
1547                            is_streaming,
1548                        );
1549                    }
1550                    continue;
1551                }
1552            }
1553        }
1554        render_assertion(
1555            out,
1556            assertion,
1557            &effective_result_var,
1558            import_alias,
1559            field_resolver,
1560            &optional_locals,
1561            result_is_simple,
1562            result_is_array,
1563            is_streaming,
1564        );
1565    }
1566
1567    let _ = writeln!(out, "}}");
1568}
1569
1570/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
1571///
1572/// Delegates to the shared driver [`client::http_call::render_http_test`] via
1573/// [`GoTestClientRenderer`]. The emitted test shape is unchanged: `func Test_<Name>(t *testing.T)`
1574/// with a `net/http` client that hits `$MOCK_SERVER_URL/fixtures/<id>`.
1575fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1576    client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1577}
1578
1579// ---------------------------------------------------------------------------
1580// HTTP test rendering — GoTestClientRenderer
1581// ---------------------------------------------------------------------------
1582
1583/// Go `net/http` test renderer.
1584///
1585/// Go HTTP e2e tests send a request to `$MOCK_SERVER_URL/fixtures/<id>` using
1586/// the standard library `net/http` client. The trait primitives emit the
1587/// request-build, response-capture, and assertion code that the previous
1588/// monolithic renderer produced, so generated output is unchanged after the
1589/// migration.
1590struct GoTestClientRenderer;
1591
1592impl client::TestClientRenderer for GoTestClientRenderer {
1593    fn language_name(&self) -> &'static str {
1594        "go"
1595    }
1596
1597    /// Go test names use `UpperCamelCase` so they form valid exported identifiers
1598    /// (e.g. `Test_MyFixtureId`). Override the default `sanitize_ident` which
1599    /// produces `lower_snake_case`.
1600    fn sanitize_test_name(&self, id: &str) -> String {
1601        id.to_upper_camel_case()
1602    }
1603
1604    /// Emit `func Test_<fn_name>(t *testing.T) {`, a description comment, and the
1605    /// `baseURL` / request scaffolding. Skipped fixtures get `t.Skip(...)` inline.
1606    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1607        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1608        let _ = writeln!(out, "\t// {description}");
1609        if let Some(reason) = skip_reason {
1610            let escaped = go_string_literal(reason);
1611            let _ = writeln!(out, "\tt.Skip({escaped})");
1612        }
1613    }
1614
1615    fn render_test_close(&self, out: &mut String) {
1616        let _ = writeln!(out, "}}");
1617    }
1618
1619    /// Emit the full `net/http` request scaffolding: URL construction, body,
1620    /// headers, cookies, a no-redirect client, and `io.ReadAll` for the body.
1621    ///
1622    /// `bodyBytes` is always declared (with `_ = bodyBytes` to avoid the Go
1623    /// "declared and not used" compile error on tests with no body assertion).
1624    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1625        let method = ctx.method.to_uppercase();
1626        let path = ctx.path;
1627
1628        let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1629        let _ = writeln!(out, "\tif baseURL == \"\" {{");
1630        let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1631        let _ = writeln!(out, "\t}}");
1632
1633        // Build request body expression.
1634        let body_expr = if let Some(body) = ctx.body {
1635            let json = serde_json::to_string(body).unwrap_or_default();
1636            let escaped = go_string_literal(&json);
1637            format!("strings.NewReader({})", escaped)
1638        } else {
1639            "strings.NewReader(\"\")".to_string()
1640        };
1641
1642        let _ = writeln!(out, "\tbody := {body_expr}");
1643        let _ = writeln!(
1644            out,
1645            "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1646        );
1647        let _ = writeln!(out, "\tif err != nil {{");
1648        let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1649        let _ = writeln!(out, "\t}}");
1650
1651        // Content-Type header (only when a body is present).
1652        if ctx.body.is_some() {
1653            let content_type = ctx.content_type.unwrap_or("application/json");
1654            let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1655        }
1656
1657        // Explicit request headers (sorted for deterministic output).
1658        let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1659        header_names.sort();
1660        for name in header_names {
1661            let value = &ctx.headers[name];
1662            let escaped_name = go_string_literal(name);
1663            let escaped_value = go_string_literal(value);
1664            let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1665        }
1666
1667        // Cookies.
1668        if !ctx.cookies.is_empty() {
1669            let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1670            cookie_names.sort();
1671            for name in cookie_names {
1672                let value = &ctx.cookies[name];
1673                let escaped_name = go_string_literal(name);
1674                let escaped_value = go_string_literal(value);
1675                let _ = writeln!(
1676                    out,
1677                    "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1678                );
1679            }
1680        }
1681
1682        // No-redirect client so 3xx fixtures assert the redirect response itself.
1683        let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1684        let _ = writeln!(
1685            out,
1686            "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1687        );
1688        let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1689        let _ = writeln!(out, "\t\t}},");
1690        let _ = writeln!(out, "\t}}");
1691        let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1692        let _ = writeln!(out, "\tif err != nil {{");
1693        let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1694        let _ = writeln!(out, "\t}}");
1695        let _ = writeln!(out, "\tdefer resp.Body.Close()");
1696
1697        // Always read the response body so body-assertion methods can reference
1698        // `bodyBytes`. Suppress the "declared and not used" compile error with
1699        // `_ = bodyBytes` for tests that have no body assertion.
1700        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1701        let _ = writeln!(out, "\tif err != nil {{");
1702        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1703        let _ = writeln!(out, "\t}}");
1704        let _ = writeln!(out, "\t_ = bodyBytes");
1705    }
1706
1707    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1708        let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1709        let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1710        let _ = writeln!(out, "\t}}");
1711    }
1712
1713    /// Emit a header assertion, skipping special tokens (`<<present>>`, `<<absent>>`,
1714    /// `<<uuid>>`) and hop-by-hop headers (`Connection`) that `net/http` strips.
1715    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1716        // Skip special-token assertions.
1717        if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1718            return;
1719        }
1720        // Connection is a hop-by-hop header that Go's net/http strips.
1721        if name.eq_ignore_ascii_case("connection") {
1722            return;
1723        }
1724        let escaped_name = go_string_literal(name);
1725        let escaped_value = go_string_literal(expected);
1726        let _ = writeln!(
1727            out,
1728            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1729        );
1730        let _ = writeln!(
1731            out,
1732            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1733        );
1734        let _ = writeln!(out, "\t}}");
1735    }
1736
1737    /// Emit an exact-equality body assertion.
1738    ///
1739    /// JSON objects and arrays are round-tripped via `json.Unmarshal` + `reflect.DeepEqual`.
1740    /// Scalar values are compared as trimmed strings.
1741    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1742        match expected {
1743            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1744                let json_str = serde_json::to_string(expected).unwrap_or_default();
1745                let escaped = go_string_literal(&json_str);
1746                let _ = writeln!(out, "\tvar got any");
1747                let _ = writeln!(out, "\tvar want any");
1748                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1749                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1750                let _ = writeln!(out, "\t}}");
1751                let _ = writeln!(
1752                    out,
1753                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1754                );
1755                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1756                let _ = writeln!(out, "\t}}");
1757                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1758                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1759                let _ = writeln!(out, "\t}}");
1760            }
1761            serde_json::Value::String(s) => {
1762                let escaped = go_string_literal(s);
1763                let _ = writeln!(out, "\twant := {escaped}");
1764                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1765                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1766                let _ = writeln!(out, "\t}}");
1767            }
1768            other => {
1769                let escaped = go_string_literal(&other.to_string());
1770                let _ = writeln!(out, "\twant := {escaped}");
1771                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1772                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1773                let _ = writeln!(out, "\t}}");
1774            }
1775        }
1776    }
1777
1778    /// Emit partial-body assertions: every key in `expected` must appear in the
1779    /// parsed JSON response with the matching value.
1780    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1781        if let Some(obj) = expected.as_object() {
1782            let _ = writeln!(out, "\tvar _partialGot map[string]any");
1783            let _ = writeln!(
1784                out,
1785                "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1786            );
1787            let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1788            let _ = writeln!(out, "\t}}");
1789            for (key, val) in obj {
1790                let escaped_key = go_string_literal(key);
1791                let json_val = serde_json::to_string(val).unwrap_or_default();
1792                let escaped_val = go_string_literal(&json_val);
1793                let _ = writeln!(out, "\t{{");
1794                let _ = writeln!(out, "\t\tvar _wantVal any");
1795                let _ = writeln!(
1796                    out,
1797                    "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1798                );
1799                let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1800                let _ = writeln!(out, "\t\t}}");
1801                let _ = writeln!(
1802                    out,
1803                    "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1804                );
1805                let _ = writeln!(
1806                    out,
1807                    "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1808                );
1809                let _ = writeln!(out, "\t\t}}");
1810                let _ = writeln!(out, "\t}}");
1811            }
1812        }
1813    }
1814
1815    /// Emit validation-error assertions for 422 responses.
1816    ///
1817    /// Checks that each expected `msg` appears in at least one element of the
1818    /// parsed body's `"errors"` array.
1819    fn render_assert_validation_errors(
1820        &self,
1821        out: &mut String,
1822        _response_var: &str,
1823        errors: &[ValidationErrorExpectation],
1824    ) {
1825        let _ = writeln!(out, "\tvar _veBody map[string]any");
1826        let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1827        let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1828        let _ = writeln!(out, "\t}}");
1829        let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1830        for ve in errors {
1831            let escaped_msg = go_string_literal(&ve.msg);
1832            let _ = writeln!(out, "\t{{");
1833            let _ = writeln!(out, "\t\t_found := false");
1834            let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1835            let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1836            let _ = writeln!(
1837                out,
1838                "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1839            );
1840            let _ = writeln!(out, "\t\t\t\t\t_found = true");
1841            let _ = writeln!(out, "\t\t\t\t\tbreak");
1842            let _ = writeln!(out, "\t\t\t\t}}");
1843            let _ = writeln!(out, "\t\t\t}}");
1844            let _ = writeln!(out, "\t\t}}");
1845            let _ = writeln!(out, "\t\tif !_found {{");
1846            let _ = writeln!(
1847                out,
1848                "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1849            );
1850            let _ = writeln!(out, "\t\t}}");
1851            let _ = writeln!(out, "\t}}");
1852        }
1853    }
1854}
1855
1856/// Build setup lines (e.g. handle creation) and the argument list for the function call.
1857///
1858/// Returns `(setup_lines, args_string)`.
1859///
1860/// `options_ptr` — when `true`, `json_object` args with an `options_type` are
1861/// passed as a Go pointer (`*OptionsType`): absent/empty → `nil`, present →
1862/// `&varName` after JSON unmarshal.
1863fn build_args_and_setup(
1864    input: &serde_json::Value,
1865    args: &[crate::config::ArgMapping],
1866    import_alias: &str,
1867    options_type: Option<&str>,
1868    fixture: &crate::fixture::Fixture,
1869    options_ptr: bool,
1870    expects_error: bool,
1871) -> (Vec<String>, String) {
1872    let fixture_id = &fixture.id;
1873    use heck::ToUpperCamelCase;
1874
1875    if args.is_empty() {
1876        return (Vec::new(), String::new());
1877    }
1878
1879    let mut setup_lines: Vec<String> = Vec::new();
1880    let mut parts: Vec<String> = Vec::new();
1881
1882    for arg in args {
1883        if arg.arg_type == "mock_url" {
1884            if fixture.has_host_root_route() {
1885                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1886                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1887                setup_lines.push(format!(
1888                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1889                    arg.name, arg.name
1890                ));
1891            } else {
1892                setup_lines.push(format!(
1893                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1894                    arg.name,
1895                ));
1896            }
1897            parts.push(arg.name.clone());
1898            continue;
1899        }
1900
1901        if arg.arg_type == "mock_url_list" {
1902            // []string of URLs: each element is either a bare path (`/seed1`) — prefixed
1903            // with the per-fixture mock-server URL at runtime — or an absolute URL kept as-is.
1904            // Mirrors the `mock_url` resolution: `MOCK_SERVER_<FIXTURE_ID>` first, then
1905            // `MOCK_SERVER_URL/fixtures/<id>`.
1906            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1907            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1908            let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1909
1910            let paths: Vec<String> = if let Some(arr) = val.as_array() {
1911                arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1912            } else {
1913                Vec::new()
1914            };
1915
1916            let paths_literal = paths.join(", ");
1917            let var_name = &arg.name;
1918
1919            setup_lines.push(format!(
1920                "{var_name}Base := os.Getenv(\"{env_key}\")\n\tif {var_name}Base == \"\" {{\n\t\t{var_name}Base = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
1921            ));
1922            setup_lines.push(format!(
1923                "var {var_name} []string\n\tfor _, p := range []string{{{paths_literal}}} {{\n\t\tif strings.HasPrefix(p, \"http\") {{\n\t\t\t{var_name} = append({var_name}, p)\n\t\t}} else {{\n\t\t\t{var_name} = append({var_name}, {var_name}Base + p)\n\t\t}}\n\t}}"
1924            ));
1925            parts.push(var_name.to_string());
1926            continue;
1927        }
1928
1929        if arg.arg_type == "handle" {
1930            // Generate a CreateEngine (or equivalent) call and pass the variable.
1931            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1932            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1933            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1934            // When the fixture expects an error (validation test), engine creation
1935            // is the error source. Assert the error and return so the test passes
1936            // without proceeding to the (unreachable) function call.
1937            let create_err_handler = if expects_error {
1938                "assert.Error(t, createErr)\n\t\treturn".to_string()
1939            } else {
1940                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1941            };
1942            if config_value.is_null()
1943                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1944            {
1945                setup_lines.push(format!(
1946                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1947                    name = arg.name,
1948                ));
1949            } else {
1950                let json_str = serde_json::to_string(config_value).unwrap_or_default();
1951                let go_literal = go_string_literal(&json_str);
1952                let name = &arg.name;
1953                setup_lines.push(format!(
1954                    "var {name}Config {import_alias}.CrawlConfig\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1955                ));
1956                setup_lines.push(format!(
1957                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1958                ));
1959            }
1960            parts.push(arg.name.clone());
1961            continue;
1962        }
1963
1964        let val: Option<&serde_json::Value> = if arg.field == "input" {
1965            Some(input)
1966        } else {
1967            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1968            input.get(field)
1969        };
1970
1971        // file_path args are fixture-relative paths under `test_documents/`. The Go test
1972        // runner's TestMain (in main_test.go) already does `os.Chdir(test_documents)` so
1973        // tests can pass these relative strings directly; no additional resolution needed.
1974
1975        // Handle bytes type: fixture stores base64-encoded bytes.
1976        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
1977        if arg.arg_type == "bytes" {
1978            let var_name = format!("{}Bytes", arg.name);
1979            match val {
1980                None | Some(serde_json::Value::Null) => {
1981                    if arg.optional {
1982                        parts.push("nil".to_string());
1983                    } else {
1984                        parts.push("[]byte{}".to_string());
1985                    }
1986                }
1987                Some(serde_json::Value::String(s)) => {
1988                    // Bytes args whose fixture value is a string are fixture-relative paths into
1989                    // the repo-root `test_documents/` directory (matching the rust e2e codegen
1990                    // convention). The Go test runner's TestMain chdirs into test_documents/, so
1991                    // a bare relative path resolves correctly via os.ReadFile.
1992                    let go_path = go_string_literal(s);
1993                    setup_lines.push(format!(
1994                        "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
1995                    ));
1996                    parts.push(var_name);
1997                }
1998                Some(other) => {
1999                    parts.push(format!("[]byte({})", json_to_go(other)));
2000                }
2001            }
2002            continue;
2003        }
2004
2005        match val {
2006            None | Some(serde_json::Value::Null) if arg.optional => {
2007                // Optional arg absent: emit Go zero/nil for the type.
2008                match arg.arg_type.as_str() {
2009                    "string" => {
2010                        // Optional string in Go bindings is *string → nil.
2011                        parts.push("nil".to_string());
2012                    }
2013                    "json_object" => {
2014                        if options_ptr {
2015                            // Pointer options type (*OptionsType): absent → nil.
2016                            parts.push("nil".to_string());
2017                        } else if let Some(opts_type) = options_type {
2018                            // Value options type: zero-value struct.
2019                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
2020                        } else {
2021                            parts.push("nil".to_string());
2022                        }
2023                    }
2024                    _ => {
2025                        parts.push("nil".to_string());
2026                    }
2027                }
2028            }
2029            None | Some(serde_json::Value::Null) => {
2030                // Required arg with no fixture value: pass a language-appropriate default.
2031                let default_val = match arg.arg_type.as_str() {
2032                    "string" => "\"\"".to_string(),
2033                    "int" | "integer" | "i64" => "0".to_string(),
2034                    "float" | "number" => "0.0".to_string(),
2035                    "bool" | "boolean" => "false".to_string(),
2036                    "json_object" => {
2037                        if options_ptr {
2038                            // Pointer options type (*OptionsType): absent → nil.
2039                            "nil".to_string()
2040                        } else if let Some(opts_type) = options_type {
2041                            format!("{import_alias}.{opts_type}{{}}")
2042                        } else {
2043                            "nil".to_string()
2044                        }
2045                    }
2046                    _ => "nil".to_string(),
2047                };
2048                parts.push(default_val);
2049            }
2050            Some(v) => {
2051                match arg.arg_type.as_str() {
2052                    "json_object" => {
2053                        // JSON arrays unmarshal into []string (Go slices).
2054                        // JSON objects with a known options_type unmarshal into that type.
2055                        let is_array = v.is_array();
2056                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2057                        if is_empty_obj {
2058                            if options_ptr {
2059                                // Pointer options type: empty object → nil.
2060                                parts.push("nil".to_string());
2061                            } else if let Some(opts_type) = options_type {
2062                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
2063                            } else {
2064                                parts.push("nil".to_string());
2065                            }
2066                        } else if is_array {
2067                            // Array type — unmarshal into a Go slice. Honor `go_type` for a
2068                            // fully explicit Go type (e.g. `"kreuzberg.BatchBytesItem"`), fall
2069                            // back to deriving the slice type from `element_type`, defaulting
2070                            // to `[]string` for unknown types.
2071                            let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2072                                // go_type is the slice element type — wrap it in [].
2073                                // If it already starts with '[' the user specified the full
2074                                // slice type; use it verbatim.
2075                                if go_t.starts_with('[') {
2076                                    go_t.to_string()
2077                                } else {
2078                                    // Qualify unqualified types (e.g., "BatchBytesItem" → "kreuzberg.BatchBytesItem")
2079                                    let qualified = if go_t.contains('.') {
2080                                        go_t.to_string()
2081                                    } else {
2082                                        format!("{import_alias}.{go_t}")
2083                                    };
2084                                    format!("[]{qualified}")
2085                                }
2086                            } else {
2087                                element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2088                            };
2089                            // Convert JSON for Go compatibility (e.g., byte arrays → base64 strings)
2090                            let converted_v = convert_json_for_go(v.clone());
2091                            let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2092                            let go_literal = go_string_literal(&json_str);
2093                            let var_name = &arg.name;
2094                            setup_lines.push(format!(
2095                                "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2096                            ));
2097                            parts.push(var_name.to_string());
2098                        } else if let Some(opts_type) = options_type {
2099                            // Object with known type — unmarshal into typed struct.
2100                            // When options_ptr is set, the Go struct uses snake_case JSON
2101                            // field tags and lowercase/snake_case enum values.  Remap the
2102                            // fixture's camelCase keys and PascalCase enum string values.
2103                            let remapped_v = if options_ptr {
2104                                convert_json_for_go(v.clone())
2105                            } else {
2106                                v.clone()
2107                            };
2108                            let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2109                            let go_literal = go_string_literal(&json_str);
2110                            let var_name = &arg.name;
2111                            setup_lines.push(format!(
2112                                "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2113                            ));
2114                            // Pass as pointer when options_ptr is set.
2115                            let arg_expr = if options_ptr {
2116                                format!("&{var_name}")
2117                            } else {
2118                                var_name.to_string()
2119                            };
2120                            parts.push(arg_expr);
2121                        } else {
2122                            parts.push(json_to_go(v));
2123                        }
2124                    }
2125                    "string" if arg.optional => {
2126                        // Optional string in Go is *string — take address of a local.
2127                        let var_name = format!("{}Val", arg.name);
2128                        let go_val = json_to_go(v);
2129                        setup_lines.push(format!("{var_name} := {go_val}"));
2130                        parts.push(format!("&{var_name}"));
2131                    }
2132                    _ => {
2133                        parts.push(json_to_go(v));
2134                    }
2135                }
2136            }
2137        }
2138    }
2139
2140    (setup_lines, parts.join(", "))
2141}
2142
2143#[allow(clippy::too_many_arguments)]
2144fn render_assertion(
2145    out: &mut String,
2146    assertion: &Assertion,
2147    result_var: &str,
2148    import_alias: &str,
2149    field_resolver: &FieldResolver,
2150    optional_locals: &std::collections::HashMap<String, String>,
2151    result_is_simple: bool,
2152    result_is_array: bool,
2153    is_streaming: bool,
2154) {
2155    // Handle synthetic / derived fields before the is_valid_for_result check
2156    // so they are never treated as struct field accesses on the result.
2157    if !result_is_simple {
2158        if let Some(f) = &assertion.field {
2159            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
2160            // We emit inline func() expressions so we don't need additional variables.
2161            let embed_deref = format!("(*{result_var})");
2162            match f.as_str() {
2163                "chunks_have_content" => {
2164                    let pred = format!(
2165                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2166                    );
2167                    match assertion.assertion_type.as_str() {
2168                        "is_true" => {
2169                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2170                        }
2171                        "is_false" => {
2172                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2173                        }
2174                        _ => {
2175                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2176                        }
2177                    }
2178                    return;
2179                }
2180                "chunks_have_embeddings" => {
2181                    let pred = format!(
2182                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Embedding == nil || len(*c.Embedding) == 0 {{ return false }} }}; return true }}()"
2183                    );
2184                    match assertion.assertion_type.as_str() {
2185                        "is_true" => {
2186                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2187                        }
2188                        "is_false" => {
2189                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2190                        }
2191                        _ => {
2192                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2193                        }
2194                    }
2195                    return;
2196                }
2197                "chunks_have_heading_context" => {
2198                    let pred = format!(
2199                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2200                    );
2201                    match assertion.assertion_type.as_str() {
2202                        "is_true" => {
2203                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2204                        }
2205                        "is_false" => {
2206                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2207                        }
2208                        _ => {
2209                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2210                        }
2211                    }
2212                    return;
2213                }
2214                "first_chunk_starts_with_heading" => {
2215                    let pred = format!(
2216                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2217                    );
2218                    match assertion.assertion_type.as_str() {
2219                        "is_true" => {
2220                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2221                        }
2222                        "is_false" => {
2223                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2224                        }
2225                        _ => {
2226                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2227                        }
2228                    }
2229                    return;
2230                }
2231                "embeddings" => {
2232                    match assertion.assertion_type.as_str() {
2233                        "count_equals" => {
2234                            if let Some(val) = &assertion.value {
2235                                if let Some(n) = val.as_u64() {
2236                                    let _ = writeln!(
2237                                        out,
2238                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2239                                    );
2240                                }
2241                            }
2242                        }
2243                        "count_min" => {
2244                            if let Some(val) = &assertion.value {
2245                                if let Some(n) = val.as_u64() {
2246                                    let _ = writeln!(
2247                                        out,
2248                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2249                                    );
2250                                }
2251                            }
2252                        }
2253                        "not_empty" => {
2254                            let _ = writeln!(
2255                                out,
2256                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2257                            );
2258                        }
2259                        "is_empty" => {
2260                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2261                        }
2262                        _ => {
2263                            let _ = writeln!(
2264                                out,
2265                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2266                            );
2267                        }
2268                    }
2269                    return;
2270                }
2271                "embedding_dimensions" => {
2272                    let expr = format!(
2273                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2274                    );
2275                    match assertion.assertion_type.as_str() {
2276                        "equals" => {
2277                            if let Some(val) = &assertion.value {
2278                                if let Some(n) = val.as_u64() {
2279                                    let _ = writeln!(
2280                                        out,
2281                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2282                                    );
2283                                }
2284                            }
2285                        }
2286                        "greater_than" => {
2287                            if let Some(val) = &assertion.value {
2288                                if let Some(n) = val.as_u64() {
2289                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2290                                }
2291                            }
2292                        }
2293                        _ => {
2294                            let _ = writeln!(
2295                                out,
2296                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2297                            );
2298                        }
2299                    }
2300                    return;
2301                }
2302                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2303                    let pred = match f.as_str() {
2304                        "embeddings_valid" => {
2305                            format!(
2306                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2307                            )
2308                        }
2309                        "embeddings_finite" => {
2310                            format!(
2311                                "func() bool {{ for _, e := range {embed_deref} {{ for _, v := range e {{ if v != v || v == float32(1.0/0.0) || v == float32(-1.0/0.0) {{ return false }} }} }}; return true }}()"
2312                            )
2313                        }
2314                        "embeddings_non_zero" => {
2315                            format!(
2316                                "func() bool {{ for _, e := range {embed_deref} {{ hasNonZero := false; for _, v := range e {{ if v != 0 {{ hasNonZero = true; break }} }}; if !hasNonZero {{ return false }} }}; return true }}()"
2317                            )
2318                        }
2319                        "embeddings_normalized" => {
2320                            format!(
2321                                "func() bool {{ for _, e := range {embed_deref} {{ var n float64; for _, v := range e {{ n += float64(v) * float64(v) }}; if n < 0.999 || n > 1.001 {{ return false }} }}; return true }}()"
2322                            )
2323                        }
2324                        _ => unreachable!(),
2325                    };
2326                    match assertion.assertion_type.as_str() {
2327                        "is_true" => {
2328                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2329                        }
2330                        "is_false" => {
2331                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2332                        }
2333                        _ => {
2334                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2335                        }
2336                    }
2337                    return;
2338                }
2339                // ---- keywords / keywords_count ----
2340                // Go ExtractionResult does not expose extracted_keywords; skip.
2341                "keywords" | "keywords_count" => {
2342                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2343                    return;
2344                }
2345                _ => {}
2346            }
2347        }
2348    }
2349
2350    // Streaming virtual fields: intercept before is_valid_for_result so they are
2351    // never skipped.  These fields resolve against the `chunks` collected-list variable.
2352    // Skip the streaming interception entirely when the call has opted out
2353    // (`[e2e.calls.<name>] streaming = false`) — `chunks` then names a plain
2354    // field on the synchronous result struct and must flow through normal
2355    // accessor resolution (e.g. `result.Chunks`).
2356    if !result_is_simple && is_streaming {
2357        if let Some(f) = &assertion.field {
2358            if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2359                if let Some(expr) =
2360                    crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2361                {
2362                    match assertion.assertion_type.as_str() {
2363                        "count_min" => {
2364                            if let Some(val) = &assertion.value {
2365                                if let Some(n) = val.as_u64() {
2366                                    let _ = writeln!(
2367                                        out,
2368                                        "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2369                                    );
2370                                }
2371                            }
2372                        }
2373                        "count_equals" => {
2374                            if let Some(val) = &assertion.value {
2375                                if let Some(n) = val.as_u64() {
2376                                    let _ = writeln!(
2377                                        out,
2378                                        "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2379                                    );
2380                                }
2381                            }
2382                        }
2383                        "equals" => {
2384                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2385                                let escaped = crate::escape::go_string_literal(s);
2386                                // Deep-path streaming-virtual fields like `tool_calls[0].function.name`
2387                                // resolve to pointer-typed Go fields (`*string`). The flat virtual
2388                                // accessors `stream_content` / `finish_reason` already return `string`.
2389                                // Wrap only the deep-path case in a safe-deref IIFE.
2390                                let is_deep_path = f.contains('.') || f.contains('[');
2391                                let safe_expr = if is_deep_path {
2392                                    format!(
2393                                        "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2394                                    )
2395                                } else {
2396                                    expr.clone()
2397                                };
2398                                let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2399                            } else if let Some(val) = &assertion.value {
2400                                if let Some(n) = val.as_u64() {
2401                                    let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2402                                }
2403                            }
2404                        }
2405                        "not_empty" => {
2406                            let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2407                        }
2408                        "is_empty" => {
2409                            let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2410                        }
2411                        "is_true" => {
2412                            let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2413                        }
2414                        "is_false" => {
2415                            let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2416                        }
2417                        "greater_than" => {
2418                            if let Some(val) = &assertion.value {
2419                                if let Some(n) = val.as_u64() {
2420                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2421                                }
2422                            }
2423                        }
2424                        "greater_than_or_equal" => {
2425                            if let Some(val) = &assertion.value {
2426                                if let Some(n) = val.as_u64() {
2427                                    let _ =
2428                                        writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2429                                }
2430                            }
2431                        }
2432                        "contains" => {
2433                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2434                                let escaped = crate::escape::go_string_literal(s);
2435                                let _ =
2436                                    writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2437                            }
2438                        }
2439                        _ => {
2440                            let _ = writeln!(
2441                                out,
2442                                "\t// streaming field '{f}': assertion type '{}' not rendered",
2443                                assertion.assertion_type
2444                            );
2445                        }
2446                    }
2447                }
2448                return;
2449            }
2450        }
2451    }
2452
2453    // Skip assertions on fields that don't exist on the result type.
2454    // When result_is_simple, all field assertions operate on the scalar result directly.
2455    if !result_is_simple {
2456        if let Some(f) = &assertion.field {
2457            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2458                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2459                return;
2460            }
2461        }
2462    }
2463
2464    let field_expr = if result_is_simple {
2465        // The result IS the value — field access is irrelevant.
2466        result_var.to_string()
2467    } else {
2468        match &assertion.field {
2469            Some(f) if !f.is_empty() => {
2470                // Use the local variable if the field was dereferenced above.
2471                if let Some(local_var) = optional_locals.get(f.as_str()) {
2472                    local_var.clone()
2473                } else {
2474                    field_resolver.accessor(f, "go", result_var)
2475                }
2476            }
2477            _ => result_var.to_string(),
2478        }
2479    };
2480
2481    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
2482    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
2483    // where metadata.headings is optional → len() needs dereference).
2484    let is_optional = assertion
2485        .field
2486        .as_ref()
2487        .map(|f| {
2488            let resolved = field_resolver.resolve(f);
2489            let check_path = resolved
2490                .strip_suffix(".length")
2491                .or_else(|| resolved.strip_suffix(".count"))
2492                .or_else(|| resolved.strip_suffix(".size"))
2493                .unwrap_or(resolved);
2494            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2495        })
2496        .unwrap_or(false);
2497
2498    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
2499    // and we'll wrap with a nil guard in the assertion handlers.
2500    // However, slices are already nil-able and should not be dereferenced.
2501    let field_is_array_for_len = assertion
2502        .field
2503        .as_ref()
2504        .map(|f| {
2505            let resolved = field_resolver.resolve(f);
2506            let check_path = resolved
2507                .strip_suffix(".length")
2508                .or_else(|| resolved.strip_suffix(".count"))
2509                .or_else(|| resolved.strip_suffix(".size"))
2510                .unwrap_or(resolved);
2511            field_resolver.is_array(check_path)
2512        })
2513        .unwrap_or(false);
2514    let field_expr =
2515        if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2516            let inner = &field_expr[4..field_expr.len() - 1];
2517            format!("len(*{inner})")
2518        } else {
2519            field_expr
2520        };
2521    // Build the nil-guard expression for the inner pointer (without len wrapper).
2522    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2523        Some(field_expr[5..field_expr.len() - 1].to_string())
2524    } else {
2525        None
2526    };
2527
2528    // For optional non-string fields that weren't dereferenced into locals,
2529    // we need to dereference the pointer in comparisons.
2530    // However, slices are already nil-able and should not be dereferenced.
2531    let field_is_slice = assertion
2532        .field
2533        .as_ref()
2534        .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2535        .unwrap_or(false);
2536    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2537        format!("*{field_expr}")
2538    } else {
2539        field_expr.clone()
2540    };
2541
2542    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
2543    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
2544    // panic by checking that the array is non-empty first.
2545    // Extract the array slice expression (everything before `[0]`).
2546    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2547        let mut array_expr = field_expr[..idx].to_string();
2548        if let Some(stripped) = array_expr.strip_prefix("len(") {
2549            array_expr = stripped.to_string();
2550        }
2551        Some(array_expr)
2552    } else {
2553        None
2554    };
2555
2556    // Render the assertion into a temporary buffer first, then wrap with the array
2557    // bounds guard (if needed) by adding one extra level of indentation.
2558    let mut assertion_buf = String::new();
2559    let out_ref = &mut assertion_buf;
2560
2561    match assertion.assertion_type.as_str() {
2562        "equals" => {
2563            if let Some(expected) = &assertion.value {
2564                let go_val = json_to_go(expected);
2565                // For string equality, trim whitespace to handle trailing newlines from the converter.
2566                if expected.is_string() {
2567                    // Wrap field expression with strings.TrimSpace() for string comparisons.
2568                    // Use string() cast to handle named string types (e.g. BatchStatus, FinishReason).
2569                    // For complex types like FormatMetadata, use fmt.Sprintf to convert.
2570                    let resolved_name = assertion
2571                        .field
2572                        .as_ref()
2573                        .map(|f| field_resolver.resolve(f))
2574                        .unwrap_or_default();
2575                    let is_struct = resolved_name.contains("FormatMetadata");
2576                    let trimmed_field = if is_struct {
2577                        // Use fmt.Sprintf for struct types instead of string cast
2578                        if is_optional && !field_expr.starts_with("len(") {
2579                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2580                        } else {
2581                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2582                        }
2583                    } else if is_optional && !field_expr.starts_with("len(") {
2584                        format!("strings.TrimSpace(string(*{field_expr}))")
2585                    } else {
2586                        format!("strings.TrimSpace(string({field_expr}))")
2587                    };
2588                    if is_optional && !field_expr.starts_with("len(") {
2589                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2590                    } else {
2591                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2592                    }
2593                } else if is_optional && !field_expr.starts_with("len(") {
2594                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2595                } else {
2596                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2597                }
2598                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2599                let _ = writeln!(out_ref, "\t}}");
2600            }
2601        }
2602        "contains" => {
2603            if let Some(expected) = &assertion.value {
2604                let go_val = json_to_go(expected);
2605                // Determine the "string view" of the field expression.
2606                // - []string (optional) → jsonString(field_expr) — Go slices are nil-able, no `*` needed
2607                // - *string → string(*field_expr)
2608                // - string → string(field_expr) (or just field_expr for plain strings)
2609                // - result_is_array (result_is_simple + array result) → jsonString(field_expr)
2610                let resolved_field = assertion.field.as_deref().unwrap_or("");
2611                let resolved_name = field_resolver.resolve(resolved_field);
2612                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2613                let is_opt =
2614                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2615                let field_for_contains = if is_opt && field_is_array {
2616                    // Go slices are nil-able directly — no pointer dereference needed.
2617                    format!("jsonString({field_expr})")
2618                } else if is_opt {
2619                    format!("fmt.Sprint(*{field_expr})")
2620                } else if field_is_array {
2621                    format!("jsonString({field_expr})")
2622                } else {
2623                    format!("fmt.Sprint({field_expr})")
2624                };
2625                if is_opt {
2626                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2627                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2628                    let _ = writeln!(
2629                        out_ref,
2630                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2631                    );
2632                    let _ = writeln!(out_ref, "\t}}");
2633                    let _ = writeln!(out_ref, "\t}}");
2634                } else {
2635                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2636                    let _ = writeln!(
2637                        out_ref,
2638                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2639                    );
2640                    let _ = writeln!(out_ref, "\t}}");
2641                }
2642            }
2643        }
2644        "contains_all" => {
2645            if let Some(values) = &assertion.values {
2646                let resolved_field = assertion.field.as_deref().unwrap_or("");
2647                let resolved_name = field_resolver.resolve(resolved_field);
2648                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2649                let is_opt =
2650                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2651                for val in values {
2652                    let go_val = json_to_go(val);
2653                    let field_for_contains = if is_opt && field_is_array {
2654                        // Go slices are nil-able directly — no pointer dereference needed.
2655                        format!("jsonString({field_expr})")
2656                    } else if is_opt {
2657                        format!("fmt.Sprint(*{field_expr})")
2658                    } else if field_is_array {
2659                        format!("jsonString({field_expr})")
2660                    } else {
2661                        format!("fmt.Sprint({field_expr})")
2662                    };
2663                    if is_opt {
2664                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2665                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2666                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2667                        let _ = writeln!(out_ref, "\t}}");
2668                        let _ = writeln!(out_ref, "\t}}");
2669                    } else {
2670                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2671                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2672                        let _ = writeln!(out_ref, "\t}}");
2673                    }
2674                }
2675            }
2676        }
2677        "not_contains" => {
2678            if let Some(expected) = &assertion.value {
2679                let go_val = json_to_go(expected);
2680                let resolved_field = assertion.field.as_deref().unwrap_or("");
2681                let resolved_name = field_resolver.resolve(resolved_field);
2682                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2683                let is_opt =
2684                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2685                let field_for_contains = if is_opt && field_is_array {
2686                    // Go slices are nil-able directly — no pointer dereference needed.
2687                    format!("jsonString({field_expr})")
2688                } else if is_opt {
2689                    format!("fmt.Sprint(*{field_expr})")
2690                } else if field_is_array {
2691                    format!("jsonString({field_expr})")
2692                } else {
2693                    format!("fmt.Sprint({field_expr})")
2694                };
2695                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2696                let _ = writeln!(
2697                    out_ref,
2698                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2699                );
2700                let _ = writeln!(out_ref, "\t}}");
2701            }
2702        }
2703        "not_empty" => {
2704            // For optional struct pointers (not arrays), just check != nil.
2705            // For optional slice/string pointers, check nil and len.
2706            let field_is_array = {
2707                let rf = assertion.field.as_deref().unwrap_or("");
2708                let rn = field_resolver.resolve(rf);
2709                field_resolver.is_array(rn)
2710            };
2711            if is_optional && !field_is_array {
2712                // Struct pointer: non-empty means not nil.
2713                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2714            } else if is_optional && field_is_slice {
2715                // Slice optional: Go slices are already nil-able — no dereference needed.
2716                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2717            } else if is_optional {
2718                // Pointer-to-slice (*[]T): dereference then len.
2719                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2720            } else if result_is_simple && result_is_array {
2721                // Simple array result ([]T) — direct slice, not a pointer; check length only.
2722                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2723            } else {
2724                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2725            }
2726            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2727            let _ = writeln!(out_ref, "\t}}");
2728        }
2729        "is_empty" => {
2730            let field_is_array = {
2731                let rf = assertion.field.as_deref().unwrap_or("");
2732                let rn = field_resolver.resolve(rf);
2733                field_resolver.is_array(rn)
2734            };
2735            // Special case: result_is_simple && !result_is_array && no field means the result is a pointer.
2736            // Empty means nil.
2737            if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2738                // Pointer result (not dereferenced): empty means nil.
2739                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2740            } else if is_optional && !field_is_array {
2741                // Struct pointer: empty means nil.
2742                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2743            } else if is_optional && field_is_slice {
2744                // Slice optional: Go slices are already nil-able — no dereference needed.
2745                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2746            } else if is_optional {
2747                // Pointer-to-slice (*[]T): dereference then len.
2748                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2749            } else {
2750                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2751            }
2752            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2753            let _ = writeln!(out_ref, "\t}}");
2754        }
2755        "contains_any" => {
2756            if let Some(values) = &assertion.values {
2757                let resolved_field = assertion.field.as_deref().unwrap_or("");
2758                let resolved_name = field_resolver.resolve(resolved_field);
2759                let field_is_array = field_resolver.is_array(resolved_name);
2760                let is_opt =
2761                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2762                let field_for_contains = if is_opt && field_is_array {
2763                    // Go slices are nil-able directly — no pointer dereference needed.
2764                    format!("jsonString({field_expr})")
2765                } else if is_opt {
2766                    format!("fmt.Sprint(*{field_expr})")
2767                } else if field_is_array {
2768                    format!("jsonString({field_expr})")
2769                } else {
2770                    format!("fmt.Sprint({field_expr})")
2771                };
2772                let _ = writeln!(out_ref, "\t{{");
2773                let _ = writeln!(out_ref, "\t\tfound := false");
2774                for val in values {
2775                    let go_val = json_to_go(val);
2776                    let _ = writeln!(
2777                        out_ref,
2778                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2779                    );
2780                }
2781                let _ = writeln!(out_ref, "\t\tif !found {{");
2782                let _ = writeln!(
2783                    out_ref,
2784                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2785                );
2786                let _ = writeln!(out_ref, "\t\t}}");
2787                let _ = writeln!(out_ref, "\t}}");
2788            }
2789        }
2790        "greater_than" => {
2791            if let Some(val) = &assertion.value {
2792                let go_val = json_to_go(val);
2793                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
2794                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
2795                // For optional (pointer) fields, dereference and guard with nil check.
2796                if is_optional {
2797                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2798                    if let Some(n) = val.as_u64() {
2799                        let next = n + 1;
2800                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2801                    } else {
2802                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2803                    }
2804                    let _ = writeln!(
2805                        out_ref,
2806                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2807                    );
2808                    let _ = writeln!(out_ref, "\t\t}}");
2809                    let _ = writeln!(out_ref, "\t}}");
2810                } else if let Some(n) = val.as_u64() {
2811                    let next = n + 1;
2812                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2813                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2814                    let _ = writeln!(out_ref, "\t}}");
2815                } else {
2816                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2817                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2818                    let _ = writeln!(out_ref, "\t}}");
2819                }
2820            }
2821        }
2822        "less_than" => {
2823            if let Some(val) = &assertion.value {
2824                let go_val = json_to_go(val);
2825                if let Some(ref guard) = nil_guard_expr {
2826                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2827                    let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2828                    let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2829                    let _ = writeln!(out_ref, "\t\t}}");
2830                    let _ = writeln!(out_ref, "\t}}");
2831                } else if is_optional && !field_expr.starts_with("len(") {
2832                    // Optional pointer field: nil-guard and dereference before comparison.
2833                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2834                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2835                    let _ = writeln!(
2836                        out_ref,
2837                        "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2838                    );
2839                    let _ = writeln!(out_ref, "\t\t}}");
2840                    let _ = writeln!(out_ref, "\t}}");
2841                } else {
2842                    let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2843                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2844                    let _ = writeln!(out_ref, "\t}}");
2845                }
2846            }
2847        }
2848        "greater_than_or_equal" => {
2849            if let Some(val) = &assertion.value {
2850                let go_val = json_to_go(val);
2851                if let Some(ref guard) = nil_guard_expr {
2852                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2853                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2854                    let _ = writeln!(
2855                        out_ref,
2856                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2857                    );
2858                    let _ = writeln!(out_ref, "\t\t}}");
2859                    let _ = writeln!(out_ref, "\t}}");
2860                } else if is_optional && !field_expr.starts_with("len(") {
2861                    // Optional pointer field: nil-guard and dereference before comparison.
2862                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2863                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2864                    let _ = writeln!(
2865                        out_ref,
2866                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2867                    );
2868                    let _ = writeln!(out_ref, "\t\t}}");
2869                    let _ = writeln!(out_ref, "\t}}");
2870                } else {
2871                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2872                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2873                    let _ = writeln!(out_ref, "\t}}");
2874                }
2875            }
2876        }
2877        "less_than_or_equal" => {
2878            if let Some(val) = &assertion.value {
2879                let go_val = json_to_go(val);
2880                if is_optional && !field_expr.starts_with("len(") {
2881                    // Optional pointer field: nil-guard and dereference before comparison.
2882                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2883                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2884                    let _ = writeln!(
2885                        out_ref,
2886                        "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2887                    );
2888                    let _ = writeln!(out_ref, "\t\t}}");
2889                    let _ = writeln!(out_ref, "\t}}");
2890                } else {
2891                    let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2892                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2893                    let _ = writeln!(out_ref, "\t}}");
2894                }
2895            }
2896        }
2897        "starts_with" => {
2898            if let Some(expected) = &assertion.value {
2899                let go_val = json_to_go(expected);
2900                let field_for_prefix = if is_optional
2901                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2902                {
2903                    format!("string(*{field_expr})")
2904                } else {
2905                    format!("string({field_expr})")
2906                };
2907                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2908                let _ = writeln!(
2909                    out_ref,
2910                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2911                );
2912                let _ = writeln!(out_ref, "\t}}");
2913            }
2914        }
2915        "count_min" => {
2916            if let Some(val) = &assertion.value {
2917                if let Some(n) = val.as_u64() {
2918                    if is_optional {
2919                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2920                        // Slices are value types in Go — use len(slice) not len(*slice).
2921                        let len_expr = if field_is_slice {
2922                            format!("len({field_expr})")
2923                        } else {
2924                            format!("len(*{field_expr})")
2925                        };
2926                        let _ = writeln!(
2927                            out_ref,
2928                            "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2929                        );
2930                        let _ = writeln!(out_ref, "\t}}");
2931                    } else {
2932                        let _ = writeln!(
2933                            out_ref,
2934                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2935                        );
2936                    }
2937                }
2938            }
2939        }
2940        "count_equals" => {
2941            if let Some(val) = &assertion.value {
2942                if let Some(n) = val.as_u64() {
2943                    if is_optional {
2944                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2945                        // Slices are value types in Go — use len(slice) not len(*slice).
2946                        let len_expr = if field_is_slice {
2947                            format!("len({field_expr})")
2948                        } else {
2949                            format!("len(*{field_expr})")
2950                        };
2951                        let _ = writeln!(
2952                            out_ref,
2953                            "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2954                        );
2955                        let _ = writeln!(out_ref, "\t}}");
2956                    } else {
2957                        let _ = writeln!(
2958                            out_ref,
2959                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2960                        );
2961                    }
2962                }
2963            }
2964        }
2965        "is_true" => {
2966            if is_optional {
2967                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2968                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2969                let _ = writeln!(out_ref, "\t}}");
2970            } else {
2971                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2972            }
2973        }
2974        "is_false" => {
2975            if is_optional {
2976                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2977                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2978                let _ = writeln!(out_ref, "\t}}");
2979            } else {
2980                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2981            }
2982        }
2983        "method_result" => {
2984            if let Some(method_name) = &assertion.method {
2985                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2986                let check = assertion.check.as_deref().unwrap_or("is_true");
2987                // For pointer-returning functions, dereference with `*`. Value-returning
2988                // functions (e.g., NodeInfo field access) are used directly.
2989                let deref_expr = if info.is_pointer {
2990                    format!("*{}", info.call_expr)
2991                } else {
2992                    info.call_expr.clone()
2993                };
2994                match check {
2995                    "equals" => {
2996                        if let Some(val) = &assertion.value {
2997                            if val.is_boolean() {
2998                                if val.as_bool() == Some(true) {
2999                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3000                                } else {
3001                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3002                                }
3003                            } else {
3004                                // Apply type cast to numeric literals when the method returns
3005                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
3006                                // mismatches between int and uint in testify's assert.Equal.
3007                                let go_val = if let Some(cast) = info.value_cast {
3008                                    if val.is_number() {
3009                                        format!("{cast}({})", json_to_go(val))
3010                                    } else {
3011                                        json_to_go(val)
3012                                    }
3013                                } else {
3014                                    json_to_go(val)
3015                                };
3016                                let _ = writeln!(
3017                                    out_ref,
3018                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
3019                                );
3020                            }
3021                        }
3022                    }
3023                    "is_true" => {
3024                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3025                    }
3026                    "is_false" => {
3027                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3028                    }
3029                    "greater_than_or_equal" => {
3030                        if let Some(val) = &assertion.value {
3031                            let n = val.as_u64().unwrap_or(0);
3032                            // Use the value_cast type if available (e.g., uint for named_children_count).
3033                            let cast = info.value_cast.unwrap_or("uint");
3034                            let _ = writeln!(
3035                                out_ref,
3036                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3037                            );
3038                        }
3039                    }
3040                    "count_min" => {
3041                        if let Some(val) = &assertion.value {
3042                            let n = val.as_u64().unwrap_or(0);
3043                            let _ = writeln!(
3044                                out_ref,
3045                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3046                            );
3047                        }
3048                    }
3049                    "contains" => {
3050                        if let Some(val) = &assertion.value {
3051                            let go_val = json_to_go(val);
3052                            let _ = writeln!(
3053                                out_ref,
3054                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3055                            );
3056                        }
3057                    }
3058                    "is_error" => {
3059                        let _ = writeln!(out_ref, "\t{{");
3060                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3061                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3062                        let _ = writeln!(out_ref, "\t}}");
3063                    }
3064                    other_check => {
3065                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3066                    }
3067                }
3068            } else {
3069                panic!("Go e2e generator: method_result assertion missing 'method' field");
3070            }
3071        }
3072        "min_length" => {
3073            if let Some(val) = &assertion.value {
3074                if let Some(n) = val.as_u64() {
3075                    if is_optional {
3076                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3077                        let _ = writeln!(
3078                            out_ref,
3079                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3080                        );
3081                        let _ = writeln!(out_ref, "\t}}");
3082                    } else if field_expr.starts_with("len(") {
3083                        let _ = writeln!(
3084                            out_ref,
3085                            "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3086                        );
3087                    } else {
3088                        let _ = writeln!(
3089                            out_ref,
3090                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3091                        );
3092                    }
3093                }
3094            }
3095        }
3096        "max_length" => {
3097            if let Some(val) = &assertion.value {
3098                if let Some(n) = val.as_u64() {
3099                    if is_optional {
3100                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3101                        let _ = writeln!(
3102                            out_ref,
3103                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3104                        );
3105                        let _ = writeln!(out_ref, "\t}}");
3106                    } else if field_expr.starts_with("len(") {
3107                        let _ = writeln!(
3108                            out_ref,
3109                            "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3110                        );
3111                    } else {
3112                        let _ = writeln!(
3113                            out_ref,
3114                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3115                        );
3116                    }
3117                }
3118            }
3119        }
3120        "ends_with" => {
3121            if let Some(expected) = &assertion.value {
3122                let go_val = json_to_go(expected);
3123                let field_for_suffix = if is_optional
3124                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3125                {
3126                    format!("string(*{field_expr})")
3127                } else {
3128                    format!("string({field_expr})")
3129                };
3130                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3131                let _ = writeln!(
3132                    out_ref,
3133                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3134                );
3135                let _ = writeln!(out_ref, "\t}}");
3136            }
3137        }
3138        "matches_regex" => {
3139            if let Some(expected) = &assertion.value {
3140                let go_val = json_to_go(expected);
3141                let field_for_regex = if is_optional
3142                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3143                {
3144                    format!("*{field_expr}")
3145                } else {
3146                    field_expr.clone()
3147                };
3148                let _ = writeln!(
3149                    out_ref,
3150                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3151                );
3152            }
3153        }
3154        "not_error" => {
3155            // Already handled by the `if err != nil` check above.
3156        }
3157        "error" => {
3158            // Handled at the test function level.
3159        }
3160        other => {
3161            panic!("Go e2e generator: unsupported assertion type: {other}");
3162        }
3163    }
3164
3165    // If the assertion accesses an array element via [0], wrap the generated code in a
3166    // bounds check to prevent an index-out-of-range panic when the array is empty.
3167    if let Some(ref arr) = array_guard {
3168        if !assertion_buf.is_empty() {
3169            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3170            // Re-indent each line by one additional tab level.
3171            for line in assertion_buf.lines() {
3172                let _ = writeln!(out, "\t{line}");
3173            }
3174            let _ = writeln!(out, "\t}}");
3175        }
3176    } else {
3177        out.push_str(&assertion_buf);
3178    }
3179}
3180
3181/// Metadata about the return type of a Go method call for `method_result` assertions.
3182struct GoMethodCallInfo {
3183    /// The call expression string.
3184    call_expr: String,
3185    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
3186    is_pointer: bool,
3187    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
3188    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
3189    value_cast: Option<&'static str>,
3190}
3191
3192/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
3193///
3194/// Maps method names to the appropriate Go function calls, matching the Go binding API
3195/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
3196/// its return type characteristics.
3197///
3198/// Return types by method:
3199/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
3200/// - `error_count` → `*uint` (pointer, value_cast = "uint")
3201/// - `tree_to_sexp` → `*string` (pointer)
3202/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
3203/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
3204/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
3205/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
3206fn build_go_method_call(
3207    result_var: &str,
3208    method_name: &str,
3209    args: Option<&serde_json::Value>,
3210    import_alias: &str,
3211) -> GoMethodCallInfo {
3212    match method_name {
3213        "root_node_type" => GoMethodCallInfo {
3214            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3215            is_pointer: false,
3216            value_cast: None,
3217        },
3218        "named_children_count" => GoMethodCallInfo {
3219            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3220            is_pointer: false,
3221            value_cast: Some("uint"),
3222        },
3223        "has_error_nodes" => GoMethodCallInfo {
3224            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3225            is_pointer: true,
3226            value_cast: None,
3227        },
3228        "error_count" | "tree_error_count" => GoMethodCallInfo {
3229            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3230            is_pointer: true,
3231            value_cast: Some("uint"),
3232        },
3233        "tree_to_sexp" => GoMethodCallInfo {
3234            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3235            is_pointer: true,
3236            value_cast: None,
3237        },
3238        "contains_node_type" => {
3239            let node_type = args
3240                .and_then(|a| a.get("node_type"))
3241                .and_then(|v| v.as_str())
3242                .unwrap_or("");
3243            GoMethodCallInfo {
3244                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3245                is_pointer: true,
3246                value_cast: None,
3247            }
3248        }
3249        "find_nodes_by_type" => {
3250            let node_type = args
3251                .and_then(|a| a.get("node_type"))
3252                .and_then(|v| v.as_str())
3253                .unwrap_or("");
3254            GoMethodCallInfo {
3255                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3256                is_pointer: true,
3257                value_cast: None,
3258            }
3259        }
3260        "run_query" => {
3261            let query_source = args
3262                .and_then(|a| a.get("query_source"))
3263                .and_then(|v| v.as_str())
3264                .unwrap_or("");
3265            let language = args
3266                .and_then(|a| a.get("language"))
3267                .and_then(|v| v.as_str())
3268                .unwrap_or("");
3269            let query_lit = go_string_literal(query_source);
3270            let lang_lit = go_string_literal(language);
3271            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
3272            GoMethodCallInfo {
3273                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3274                is_pointer: false,
3275                value_cast: None,
3276            }
3277        }
3278        other => {
3279            let method_pascal = other.to_upper_camel_case();
3280            GoMethodCallInfo {
3281                call_expr: format!("{result_var}.{method_pascal}()"),
3282                is_pointer: false,
3283                value_cast: None,
3284            }
3285        }
3286    }
3287}
3288
3289/// Convert a `serde_json::Value` to a Go literal string.
3290/// Recursively convert a JSON value for Go struct unmarshalling.
3291///
3292/// The Go binding's `ConversionOptions` struct uses:
3293/// - `snake_case` JSON field tags (e.g. `"code_block_style"` not `"codeBlockStyle"`)
3294/// - lowercase/snake_case string values for enums (e.g. `"indented"`, `"atx_closed"`)
3295///
3296/// Fixture JSON uses camelCase keys and PascalCase enum values (Python/TS conventions).
3297/// This function remaps both so the generated Go tests can unmarshal correctly.
3298fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3299    match value {
3300        serde_json::Value::Object(map) => {
3301            let new_map: serde_json::Map<String, serde_json::Value> = map
3302                .into_iter()
3303                .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3304                .collect();
3305            serde_json::Value::Object(new_map)
3306        }
3307        serde_json::Value::Array(arr) => {
3308            // Check if this is a byte array (array of integers 0-255).
3309            // If so, encode as base64 string for Go json.Unmarshal compatibility.
3310            if is_byte_array(&arr) {
3311                let bytes: Vec<u8> = arr
3312                    .iter()
3313                    .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3314                    .collect();
3315                // Encode bytes as base64 for Go json.Unmarshal (Go expects []byte as base64 strings)
3316                let encoded = base64_encode(&bytes);
3317                serde_json::Value::String(encoded)
3318            } else {
3319                serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3320            }
3321        }
3322        serde_json::Value::String(s) => {
3323            // Convert PascalCase enum values to snake_case.
3324            // Only convert values that look like PascalCase (start with uppercase, no spaces).
3325            serde_json::Value::String(pascal_to_snake_case(&s))
3326        }
3327        other => other,
3328    }
3329}
3330
3331/// Check if an array looks like a byte array (all elements are integers 0-255).
3332fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3333    if arr.is_empty() {
3334        return false;
3335    }
3336    arr.iter().all(|v| {
3337        if let serde_json::Value::Number(n) = v {
3338            n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3339        } else {
3340            false
3341        }
3342    })
3343}
3344
3345/// Encode bytes as base64 string (standard alphabet without padding in this output,
3346/// though Go's json.Unmarshal handles both).
3347fn base64_encode(bytes: &[u8]) -> String {
3348    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3349    let mut result = String::new();
3350    let mut i = 0;
3351
3352    while i + 2 < bytes.len() {
3353        let b1 = bytes[i];
3354        let b2 = bytes[i + 1];
3355        let b3 = bytes[i + 2];
3356
3357        result.push(TABLE[(b1 >> 2) as usize] as char);
3358        result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3359        result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3360        result.push(TABLE[(b3 & 0x3f) as usize] as char);
3361
3362        i += 3;
3363    }
3364
3365    // Handle remaining bytes
3366    if i < bytes.len() {
3367        let b1 = bytes[i];
3368        result.push(TABLE[(b1 >> 2) as usize] as char);
3369
3370        if i + 1 < bytes.len() {
3371            let b2 = bytes[i + 1];
3372            result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3373            result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3374            result.push('=');
3375        } else {
3376            result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3377            result.push_str("==");
3378        }
3379    }
3380
3381    result
3382}
3383
3384/// Convert a camelCase or PascalCase string to snake_case.
3385fn camel_to_snake_case(s: &str) -> String {
3386    let mut result = String::new();
3387    let mut prev_upper = false;
3388    for (i, c) in s.char_indices() {
3389        if c.is_uppercase() {
3390            if i > 0 && !prev_upper {
3391                result.push('_');
3392            }
3393            result.push(c.to_lowercase().next().unwrap_or(c));
3394            prev_upper = true;
3395        } else {
3396            if prev_upper && i > 1 {
3397                // Handles sequences like "URLPath" → "url_path": insert _ before last uppercase
3398                // when transitioning from a run of uppercase back to lowercase.
3399                // This is tricky — use simple approach: detect Aa pattern.
3400            }
3401            result.push(c);
3402            prev_upper = false;
3403        }
3404    }
3405    result
3406}
3407
3408/// Convert a PascalCase string to snake_case (for enum values).
3409///
3410/// Only converts if the string looks like PascalCase (starts uppercase, no spaces/underscores).
3411/// Values that are already lowercase/snake_case are returned unchanged.
3412fn pascal_to_snake_case(s: &str) -> String {
3413    // Skip conversion for strings that already contain underscores, spaces, or start lowercase.
3414    let first_char = s.chars().next();
3415    if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3416        return s.to_string();
3417    }
3418    camel_to_snake_case(s)
3419}
3420
3421/// Map an `ArgMapping.element_type` to a Go slice type. Used for `json_object` args
3422/// whose fixture value is a JSON array. The element type is wrapped in `[]…` so an
3423/// element of `String` becomes `[]string` and `Vec<String>` becomes `[][]string`.
3424fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3425    let elem = element_type.unwrap_or("String").trim();
3426    let go_elem = rust_type_to_go(elem, import_alias);
3427    format!("[]{go_elem}")
3428}
3429
3430/// Map a small subset of Rust scalar / `Vec<T>` types to their Go equivalents.
3431/// For unknown types, qualify with the import alias (e.g., "kreuzberg.BatchBytesItem").
3432fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3433    let trimmed = rust.trim();
3434    if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3435        return format!("[]{}", rust_type_to_go(inner, import_alias));
3436    }
3437    match trimmed {
3438        "String" | "&str" | "str" => "string".to_string(),
3439        "bool" => "bool".to_string(),
3440        "f32" => "float32".to_string(),
3441        "f64" => "float64".to_string(),
3442        "i8" => "int8".to_string(),
3443        "i16" => "int16".to_string(),
3444        "i32" => "int32".to_string(),
3445        "i64" | "isize" => "int64".to_string(),
3446        "u8" => "uint8".to_string(),
3447        "u16" => "uint16".to_string(),
3448        "u32" => "uint32".to_string(),
3449        "u64" | "usize" => "uint64".to_string(),
3450        _ => format!("{import_alias}.{trimmed}"),
3451    }
3452}
3453
3454fn json_to_go(value: &serde_json::Value) -> String {
3455    match value {
3456        serde_json::Value::String(s) => go_string_literal(s),
3457        serde_json::Value::Bool(b) => b.to_string(),
3458        serde_json::Value::Number(n) => n.to_string(),
3459        serde_json::Value::Null => "nil".to_string(),
3460        // For complex types, serialize to JSON string and pass as literal.
3461        other => go_string_literal(&other.to_string()),
3462    }
3463}
3464
3465// ---------------------------------------------------------------------------
3466// Visitor generation
3467// ---------------------------------------------------------------------------
3468
3469/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
3470///
3471/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
3472/// polluting the exported API of the test package while still being package-level).
3473fn visitor_struct_name(fixture_id: &str) -> String {
3474    use heck::ToUpperCamelCase;
3475    // Use UpperCamelCase so Go treats it as exported — required for method sets.
3476    format!("testVisitor{}", fixture_id.to_upper_camel_case())
3477}
3478
3479/// Emit a package-level Go struct declaration and all its visitor methods.
3480///
3481/// The struct embeds `BaseVisitor` to satisfy all interface methods not
3482/// explicitly overridden by the fixture callbacks.
3483fn emit_go_visitor_struct(
3484    out: &mut String,
3485    struct_name: &str,
3486    visitor_spec: &crate::fixture::VisitorSpec,
3487    import_alias: &str,
3488) {
3489    let _ = writeln!(out, "type {struct_name} struct{{");
3490    let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3491    let _ = writeln!(out, "}}");
3492    for (method_name, action) in &visitor_spec.callbacks {
3493        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3494    }
3495}
3496
3497/// Emit a Go visitor method for a callback action on the named struct.
3498fn emit_go_visitor_method(
3499    out: &mut String,
3500    struct_name: &str,
3501    method_name: &str,
3502    action: &CallbackAction,
3503    import_alias: &str,
3504) {
3505    let camel_method = method_to_camel(method_name);
3506    // Parameter signatures must exactly match the htmltomarkdown.Visitor interface.
3507    // Optional fields use pointer types (*string, *uint32, etc.) to indicate nil-ability.
3508    let params = match method_name {
3509        "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3510        "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3511        "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3512        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3513        "visit_code_inline"
3514        | "visit_strong"
3515        | "visit_emphasis"
3516        | "visit_strikethrough"
3517        | "visit_underline"
3518        | "visit_subscript"
3519        | "visit_superscript"
3520        | "visit_mark"
3521        | "visit_button"
3522        | "visit_summary"
3523        | "visit_figcaption"
3524        | "visit_definition_term"
3525        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3526        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3527        "visit_list_item" => {
3528            format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3529        }
3530        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3531        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3532        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3533        "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3534        "visit_input" => {
3535            format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3536        }
3537        "visit_audio" | "visit_video" | "visit_iframe" => {
3538            format!("_ {import_alias}.NodeContext, src *string")
3539        }
3540        "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3541        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3542            format!("_ {import_alias}.NodeContext, output string")
3543        }
3544        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3545        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3546        _ => format!("_ {import_alias}.NodeContext"),
3547    };
3548
3549    let _ = writeln!(
3550        out,
3551        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3552    );
3553    match action {
3554        CallbackAction::Skip => {
3555            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3556        }
3557        CallbackAction::Continue => {
3558            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3559        }
3560        CallbackAction::PreserveHtml => {
3561            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3562        }
3563        CallbackAction::Custom { output } => {
3564            let escaped = go_string_literal(output);
3565            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3566        }
3567        CallbackAction::CustomTemplate { template, .. } => {
3568            // Convert {var} placeholders to %s format verbs and collect arg names.
3569            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
3570            //
3571            // For pointer-typed params (e.g. `src *string`), dereference with `*`
3572            // — the test fixtures always supply a non-nil value for methods that
3573            // fire a custom template, so this is safe in practice.
3574            let ptr_params = go_visitor_ptr_params(method_name);
3575            let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3576            let escaped_fmt = go_string_literal(&fmt_str);
3577            if fmt_args.is_empty() {
3578                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3579            } else {
3580                let args_str = fmt_args.join(", ");
3581                let _ = writeln!(
3582                    out,
3583                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3584                );
3585            }
3586        }
3587    }
3588    let _ = writeln!(out, "}}");
3589}
3590
3591/// Return the set of camelCase parameter names that are pointer types (`*string`) for a
3592/// given visitor method name.  Used to dereference pointers in template `fmt.Sprintf` calls.
3593fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3594    match method_name {
3595        "visit_link" => ["title"].into(),
3596        "visit_image" => ["title"].into(),
3597        "visit_heading" => ["id"].into(),
3598        "visit_code_block" => ["lang"].into(),
3599        "visit_form" => ["action", "method"].into(),
3600        "visit_input" => ["name", "value"].into(),
3601        "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3602        _ => std::collections::HashSet::new(),
3603    }
3604}
3605
3606/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
3607///
3608/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
3609///
3610/// Placeholder names in the template use snake_case (matching fixture field names); they
3611/// are converted to Go camelCase parameter names using `go_param_name` so they match the
3612/// generated visitor method signatures (e.g. `{input_type}` → `inputType`).
3613///
3614/// `ptr_params` — camelCase names of parameters that are `*string`; these are
3615/// dereferenced with `*` when used as `fmt.Sprintf` arguments.  The fixtures that
3616/// use `custom_template` on pointer-param methods always supply a non-nil value.
3617fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3618    let mut fmt_str = String::new();
3619    let mut args: Vec<String> = Vec::new();
3620    let mut chars = template.chars().peekable();
3621    while let Some(c) = chars.next() {
3622        if c == '{' {
3623            // Collect placeholder name until '}'.
3624            let mut name = String::new();
3625            for inner in chars.by_ref() {
3626                if inner == '}' {
3627                    break;
3628                }
3629                name.push(inner);
3630            }
3631            fmt_str.push_str("%s");
3632            // Convert snake_case placeholder to Go camelCase to match method param names.
3633            let go_name = go_param_name(&name);
3634            // Dereference pointer params so fmt.Sprintf receives a string value.
3635            let arg_expr = if ptr_params.contains(go_name.as_str()) {
3636                format!("*{go_name}")
3637            } else {
3638                go_name
3639            };
3640            args.push(arg_expr);
3641        } else {
3642            fmt_str.push(c);
3643        }
3644    }
3645    (fmt_str, args)
3646}
3647
3648/// Convert snake_case method names to Go camelCase.
3649fn method_to_camel(snake: &str) -> String {
3650    use heck::ToUpperCamelCase;
3651    snake.to_upper_camel_case()
3652}
3653
3654#[cfg(test)]
3655mod tests {
3656    use super::*;
3657    use crate::config::{CallConfig, E2eConfig};
3658    use crate::fixture::{Assertion, Fixture};
3659
3660    fn make_fixture(id: &str) -> Fixture {
3661        Fixture {
3662            id: id.to_string(),
3663            category: None,
3664            description: "test fixture".to_string(),
3665            tags: vec![],
3666            skip: None,
3667            env: None,
3668            call: None,
3669            input: serde_json::Value::Null,
3670            mock_response: Some(crate::fixture::MockResponse {
3671                status: 200,
3672                body: Some(serde_json::Value::Null),
3673                stream_chunks: None,
3674                headers: std::collections::HashMap::new(),
3675            }),
3676            source: String::new(),
3677            http: None,
3678            assertions: vec![Assertion {
3679                assertion_type: "not_error".to_string(),
3680                ..Default::default()
3681            }],
3682            visitor: None,
3683        }
3684    }
3685
3686    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
3687    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
3688    /// instead of `clean_extracted_text`).
3689    #[test]
3690    fn test_go_method_name_uses_go_casing() {
3691        let e2e_config = E2eConfig {
3692            call: CallConfig {
3693                function: "clean_extracted_text".to_string(),
3694                module: "github.com/example/mylib".to_string(),
3695                result_var: "result".to_string(),
3696                returns_result: true,
3697                ..CallConfig::default()
3698            },
3699            ..E2eConfig::default()
3700        };
3701
3702        let fixture = make_fixture("basic_text");
3703        let mut out = String::new();
3704        render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3705
3706        assert!(
3707            out.contains("kreuzberg.CleanExtractedText("),
3708            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3709        );
3710        assert!(
3711            !out.contains("kreuzberg.clean_extracted_text("),
3712            "must not emit raw snake_case method name, got:\n{out}"
3713        );
3714    }
3715
3716    #[test]
3717    fn test_streaming_fixture_emits_collect_snippet() {
3718        // A streaming fixture should emit `stream, err :=` and the collect loop.
3719        let streaming_fixture_json = r#"{
3720            "id": "basic_stream",
3721            "description": "basic streaming test",
3722            "call": "chat_stream",
3723            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3724            "mock_response": {
3725                "status": 200,
3726                "stream_chunks": [{"delta": "hello"}]
3727            },
3728            "assertions": [
3729                {"type": "count_min", "field": "chunks", "value": 1}
3730            ]
3731        }"#;
3732        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3733        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3734
3735        let e2e_config = E2eConfig {
3736            call: CallConfig {
3737                function: "chat_stream".to_string(),
3738                module: "github.com/example/mylib".to_string(),
3739                result_var: "result".to_string(),
3740                returns_result: true,
3741                r#async: true,
3742                ..CallConfig::default()
3743            },
3744            ..E2eConfig::default()
3745        };
3746
3747        let mut out = String::new();
3748        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3749
3750        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3751        assert!(
3752            out.contains("for chunk := range stream"),
3753            "should emit collect loop, got:\n{out}"
3754        );
3755    }
3756
3757    #[test]
3758    fn test_streaming_with_client_factory_and_json_arg() {
3759        // Mimics the real liter-llm setup: no returns_result on the call,
3760        // json_object arg (binding_returns_error=true), and client_factory from
3761        // the default Go call override.
3762        use alef_core::config::e2e::{ArgMapping, CallOverride};
3763        let streaming_fixture_json = r#"{
3764            "id": "basic_stream_client",
3765            "description": "basic streaming test with client",
3766            "call": "chat_stream",
3767            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3768            "mock_response": {
3769                "status": 200,
3770                "stream_chunks": [{"delta": "hello"}]
3771            },
3772            "assertions": [
3773                {"type": "count_min", "field": "chunks", "value": 1}
3774            ]
3775        }"#;
3776        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3777        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3778
3779        let go_override = CallOverride {
3780            client_factory: Some("CreateClient".to_string()),
3781            ..Default::default()
3782        };
3783
3784        let mut call_overrides = std::collections::HashMap::new();
3785        call_overrides.insert("go".to_string(), go_override);
3786
3787        let e2e_config = E2eConfig {
3788            call: CallConfig {
3789                function: "chat_stream".to_string(),
3790                module: "github.com/example/mylib".to_string(),
3791                result_var: "result".to_string(),
3792                returns_result: false, // NOT true — like real liter-llm
3793                r#async: true,
3794                args: vec![ArgMapping {
3795                    name: "request".to_string(),
3796                    field: "input".to_string(),
3797                    arg_type: "json_object".to_string(),
3798                    optional: false,
3799                    owned: true,
3800                    element_type: None,
3801                    go_type: None,
3802                }],
3803                overrides: call_overrides,
3804                ..CallConfig::default()
3805            },
3806            ..E2eConfig::default()
3807        };
3808
3809        let mut out = String::new();
3810        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3811
3812        eprintln!("generated:\n{out}");
3813        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3814        assert!(
3815            out.contains("for chunk := range stream"),
3816            "should emit collect loop, got:\n{out}"
3817        );
3818    }
3819
3820    /// When `segments` is an optional field (Option<Vec<T>>) and a fixture asserts on
3821    /// `segments[0].id`, the prefix guard must be `result.Segments != nil` — NOT
3822    /// `result.Segments[0] != nil`, which is a compile error for a value-typed element.
3823    #[test]
3824    fn test_indexed_element_prefix_guard_uses_array_not_element() {
3825        let mut optional_fields = std::collections::HashSet::new();
3826        optional_fields.insert("segments".to_string());
3827        let mut array_fields = std::collections::HashSet::new();
3828        array_fields.insert("segments".to_string());
3829
3830        let e2e_config = E2eConfig {
3831            call: CallConfig {
3832                function: "transcribe".to_string(),
3833                module: "github.com/example/mylib".to_string(),
3834                result_var: "result".to_string(),
3835                returns_result: true,
3836                ..CallConfig::default()
3837            },
3838            fields_optional: optional_fields,
3839            fields_array: array_fields,
3840            ..E2eConfig::default()
3841        };
3842
3843        let fixture = Fixture {
3844            id: "edge_transcribe_with_timestamps".to_string(),
3845            category: None,
3846            description: "Transcription with timestamp segments".to_string(),
3847            tags: vec![],
3848            skip: None,
3849            env: None,
3850            call: None,
3851            input: serde_json::Value::Null,
3852            mock_response: Some(crate::fixture::MockResponse {
3853                status: 200,
3854                body: Some(serde_json::Value::Null),
3855                stream_chunks: None,
3856                headers: std::collections::HashMap::new(),
3857            }),
3858            source: String::new(),
3859            http: None,
3860            assertions: vec![
3861                Assertion {
3862                    assertion_type: "not_error".to_string(),
3863                    ..Default::default()
3864                },
3865                Assertion {
3866                    assertion_type: "equals".to_string(),
3867                    field: Some("segments[0].id".to_string()),
3868                    value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3869                    ..Default::default()
3870                },
3871            ],
3872            visitor: None,
3873        };
3874
3875        let mut out = String::new();
3876        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3877
3878        eprintln!("generated:\n{out}");
3879
3880        // Must guard on the slice itself — not on the element.
3881        // Accepts either `Segments != nil` or `len(Segments) > 0`; both are
3882        // valid Go guards for the slice and avoid the invalid element nil
3883        // check.
3884        assert!(
3885            out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3886            "guard must be on Segments (the slice), not an element; got:\n{out}"
3887        );
3888        // Must NOT emit the invalid element nil check.
3889        assert!(
3890            !out.contains("result.Segments[0] != nil"),
3891            "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3892        );
3893    }
3894}