File: AtsGoCodeGeneratorTests.cs
Web Access
Project: src\tests\Aspire.Hosting.CodeGeneration.Go.Tests\Aspire.Hosting.CodeGeneration.Go.Tests.csproj (Aspire.Hosting.CodeGeneration.Go.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.IO.Pipes;
using System.Net.Sockets;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.RemoteHost;
using Aspire.TestUtilities;
using Aspire.TypeSystem;
using Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes;
 
namespace Aspire.Hosting.CodeGeneration.Go.Tests;
 
[RequiresTools(["gofmt"])]
public class AtsGoCodeGeneratorTests
{
    private readonly AtsGoCodeGenerator _generator = new();
 
    // The test types are compiled into this assembly via Compile Include
    private const string TestTypesAssemblyName = "Aspire.Hosting.CodeGeneration.Go.Tests";
 
    [Fact]
    public void Language_ReturnsGo()
    {
        Assert.Equal("Go", _generator.Language);
    }
 
    [Fact]
    public async Task GenerateDistributedApplication_WithTestTypes_GeneratesCorrectOutput()
    {
        // Arrange
        var atsContext = CreateContextFromTestAssembly();
 
        // Act
        var files = _generator.GenerateDistributedApplication(atsContext);
 
        // Assert
        Assert.Equal(
            ["go.mod", "transport.go", "base.go", "aspire.go"],
            files.Keys);
 
        var generatedCode = string.Join(
            "\n",
            files.OrderBy(file => file.Key, StringComparer.Ordinal)
                .Select(file => $"// ===== {file.Key} =====\n{file.Value}"));
 
        await Verify(generatedCode, extension: "go")
            .UseFileName("AtsGeneratedAspire");
    }
 
    [Fact]
    public void GenerateDistributedApplication_WritesFilesWithLfLineEndings()
    {
        var files = _generator.GenerateDistributedApplication(CreateContextFromTestAssembly());
 
        // The files mix raw string literals, StringWriter output and embedded resources, which take the line endings
        // of the machine that built or runs the generator (CRLF on Windows), while gofmt reports CRLF as a
        // formatting difference.
        Assert.Empty(files.Where(file => file.Value.Contains('\r')).Select(file => file.Key));
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedTerminalDto_RoundTripsCallbackWireNames()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        try
        {
            var files = _generator.GenerateDistributedApplication(CreateContextFromBothAssemblies());
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "terminal_test.go"), """
                package aspire
 
                import (
                    "encoding/json"
                    "reflect"
                    "testing"
                )
 
                func TestTerminalDto(t *testing.T) {
                    var payload map[string]any
                    if err := json.Unmarshal([]byte(`{"p0":{"columns":132,"rows":50}}`), &payload); err != nil {
                        t.Fatal(err)
                    }
                    options := callbackArg[*TerminalOptions]([]any{payload["p0"]}, 0)
                    if options == nil || options.Columns != 132 || options.Rows != 50 {
                        t.Fatalf("unexpected callback argument: %#v", options)
                    }
                    options.Columns = 200
                    result := map[string]any{"p0": serializeValue(options)}
                    expected := map[string]any{"p0": map[string]any{"columns": 200, "rows": 50}}
                    if !reflect.DeepEqual(expected, result) {
                        t.Fatalf("expected %#v, got %#v", expected, result)
                    }
                }
                """, timeout.Token);
            using var process = new Process
            {
                StartInfo = new ProcessStartInfo("go")
                {
                    WorkingDirectory = directory.FullName,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    ArgumentList = { "test", "-timeout", "30s", "-run", "^TestTerminalDto$", "." }
                }
            };
            process.Start();
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go terminal callback test failed:{Environment.NewLine}{await stdout}{await stderr}");
            }
            finally
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedTransport_SupportsDuplexCommunication()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        using var process = new Process();
        try
        {
            var files = _generator.GenerateDistributedApplication(new AtsContext
            {
                Capabilities = [],
                HandleTypes = [],
                EnumTypes = [],
                DtoTypes = []
            });
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "transport_test.go"), """
                package aspire
 
                import (
                    "io"
                    "os"
                    "testing"
                    "time"
                )
 
                func TestDuplexConnection(t *testing.T) {
                    conn, err := openConnection(os.Getenv("TEST_SOCKET_PATH"), 5*time.Second)
                    if err != nil {
                        t.Fatal(err)
                    }
                    defer conn.Close()
 
                    // Deadline support proves the Windows handle is pollable rather than
                    // synchronous, without relying on which I/O goroutine runs first.
                    deadlines, ok := conn.(interface { SetDeadline(time.Time) error })
                    if !ok {
                        t.Fatal("connection does not support deadlines")
                    }
                    if err := deadlines.SetDeadline(time.Now().Add(10*time.Second)); err != nil {
                        t.Fatal(err)
                    }
 
                    received := make(chan error, 1)
                    go func() {
                        response := make([]byte, 4)
                        _, err := io.ReadFull(conn, response)
                        if err == nil && string(response) != "pong" {
                            t.Errorf("expected pong, got %q", response)
                        }
                        received <- err
                    }()
                    if _, err := conn.Write([]byte("ping")); err != nil {
                        t.Fatal(err)
                    }
                    if err := <-received; err != nil {
                        t.Fatal(err)
                    }
 
                    // Closing a connection must release a pending background read.
                    go func() {
                        _, err := conn.Read(make([]byte, 1))
                        received <- err
                    }()
                    if err := conn.Close(); err != nil {
                        t.Fatal(err)
                    }
                    select {
                    case err := <-received:
                        if err == nil {
                            t.Fatal("expected the read to fail after close")
                        }
                    case <-time.After(5*time.Second):
                        t.Fatal("close did not unblock the reader")
                    }
                }
                """, timeout.Token);
 
            var socketPath = OperatingSystem.IsWindows()
                ? $"aspire-go-{Guid.NewGuid():N}"
                : Path.Combine(directory.FullName, "socket");
            using var pipe = OperatingSystem.IsWindows()
                ? new NamedPipeServerStream(socketPath, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)
                : null;
            using var socket = OperatingSystem.IsWindows()
                ? null
                : new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
            if (socket is not null)
            {
                socket.Bind(new UnixDomainSocketEndPoint(socketPath));
                socket.Listen(1);
            }
 
            process.StartInfo = new ProcessStartInfo("go")
            {
                WorkingDirectory = directory.FullName,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                ArgumentList = { "test", "-timeout", "30s", "." },
                Environment = { ["TEST_SOCKET_PATH"] = socketPath }
            };
            process.Start();
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            var exchange = ExchangeAsync();
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go transport test failed:{Environment.NewLine}{await stdout}{await stderr}");
                await exchange;
            }
            finally
            {
                await timeout.CancelAsync();
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
                await exchange.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ContinueOnCapturedContext);
            }
 
            async Task ExchangeAsync()
            {
                using var stream = pipe is not null
                    ? await AcceptPipeAsync()
                    : new NetworkStream(await socket!.AcceptAsync(timeout.Token), ownsSocket: true);
                var request = new byte[4];
                await stream.ReadExactlyAsync(request, timeout.Token);
                Assert.Equal("ping"u8.ToArray(), request);
                await stream.WriteAsync("pong"u8.ToArray(), timeout.Token);
 
                // Keep the server connected until the client closes its pending read.
                Assert.Equal(0, await stream.ReadAsync(new byte[1], timeout.Token));
            }
 
            async Task<Stream> AcceptPipeAsync()
            {
                await pipe!.WaitForConnectionAsync(timeout.Token);
                return pipe;
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedRuntime_PassesGoRuntimeHardeningTests()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        try
        {
            var files = _generator.GenerateDistributedApplication(CreateContextFromTestAssembly());
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "transport_hardening_test.go"), """
                package aspire
 
                import (
                    "bufio"
                    "bytes"
                    "context"
                    "encoding/json"
                    "errors"
                    "fmt"
                    "math"
                    "net"
                    "runtime"
                    "strconv"
                    "strings"
                    "sync"
                    "testing"
                    "time"
                )
 
                // hostTimeout bounds one read or write by the fake host, so a stuck exchange fails
                // quickly instead of waiting for the go test timeout. It is applied per operation:
                // a single deadline set at construction would also cut off a long loop on a slow runner.
                const hostTimeout = 10 * time.Second
 
                // fakeHost is the AppHost end of an in-memory connection to a client.
                type fakeHost struct {
                    t      *testing.T
                    peer   net.Conn
                    reader *bufio.Reader
                }
 
                func newFakeHost(t *testing.T) (*client, *fakeHost) {
                    t.Helper()
                    c := newClient("")
                    server, peer := net.Pipe()
                    conn := newConnection(server, c, c.onConnectionClose)
                    c.conn = conn
                    conn.start()
                    t.Cleanup(func() {
                        c.disconnect()
                        peer.Close()
                    })
                    return c, &fakeHost{t: t, peer: peer, reader: bufio.NewReader(peer)}
                }
 
                func (h *fakeHost) read() map[string]any {
                    h.t.Helper()
                    if err := h.peer.SetReadDeadline(time.Now().Add(hostTimeout)); err != nil {
                        h.t.Fatal(err)
                    }
                    msg, err := readMessage(h.reader)
                    if err != nil {
                        h.t.Fatalf("reading from the client: %v", err)
                    }
                    return msg
                }
 
                func (h *fakeHost) write(msg map[string]any) {
                    h.t.Helper()
                    if err := h.peer.SetWriteDeadline(time.Now().Add(hostTimeout)); err != nil {
                        h.t.Fatal(err)
                    }
                    if err := writeMessage(h.peer, msg); err != nil {
                        h.t.Fatalf("writing to the client: %v", err)
                    }
                }
 
                func callbackRequest(id int, callbackID string, args any) map[string]any {
                    return map[string]any{"jsonrpc": "2.0", "id": id, "method": "invokeCallback", "params": []any{callbackID, args}}
                }
 
                func TestReadMessagePreservesIntegerBoundaries(t *testing.T) {
                    cases := []struct {
                        name    string
                        payload string
                        decode  func(any) error
                    }{
                        {
                            name:    "int64 minimum",
                            payload: `{"jsonrpc":"2.0","id":1,"result":-9223372036854775808}`,
                            decode: func(raw any) error {
                                value, err := decodeAs[int64](raw)
                                if err == nil && value != math.MinInt64 {
                                    return fmt.Errorf("got %d, want %d", value, int64(math.MinInt64))
                                }
                                return err
                            },
                        },
                        {
                            name:    "int64 maximum",
                            payload: `{"jsonrpc":"2.0","id":1,"result":9223372036854775807}`,
                            decode: func(raw any) error {
                                value, err := decodeAs[int64](raw)
                                if err == nil && value != math.MaxInt64 {
                                    return fmt.Errorf("got %d, want %d", value, int64(math.MaxInt64))
                                }
                                return err
                            },
                        },
                        {
                            name:    "uint64 maximum",
                            payload: `{"jsonrpc":"2.0","id":1,"result":18446744073709551615}`,
                            decode: func(raw any) error {
                                value, err := decodeAs[uint64](raw)
                                if err == nil && value != math.MaxUint64 {
                                    return fmt.Errorf("got %d, want %d", value, uint64(math.MaxUint64))
                                }
                                return err
                            },
                        },
                    }
 
                    for _, tc := range cases {
                        t.Run(tc.name, func(t *testing.T) {
                            var wire bytes.Buffer
                            if err := writeMessageBody(&wire, []byte(tc.payload)); err != nil {
                                t.Fatal(err)
                            }
                            message, err := readMessage(bufio.NewReader(&wire))
                            if err != nil {
                                t.Fatal(err)
                            }
                            if err := tc.decode(message["result"]); err != nil {
                                t.Fatal(err)
                            }
                        })
                    }
                }
 
                func TestUntypedNumbersRoundTripAsNumbers(t *testing.T) {
                    const payload = `{"jsonrpc":"2.0","id":1,"result":{"nested":[9007199254740993,1.5]}}`
                    var wire bytes.Buffer
                    if err := writeMessageBody(&wire, []byte(payload)); err != nil {
                        t.Fatal(err)
                    }
                    message, err := readMessage(bufio.NewReader(&wire))
                    if err != nil {
                        t.Fatal(err)
                    }
 
                    encoded, err := json.Marshal(serializeValue(message["result"]))
                    if err != nil {
                        t.Fatal(err)
                    }
                    if got, want := string(encoded), `{"nested":[9007199254740993,1.5]}`; got != want {
                        t.Fatalf("got %s, want %s", got, want)
                    }
                }
 
                func TestReadMessageRejectsTrailingJson(t *testing.T) {
                    for _, payload := range []string{
                        `{"jsonrpc":"2.0","id":1,"result":null}{}`,
                        `{"jsonrpc":"2.0","id":1,"result":null}garbage`,
                    } {
                        var wire bytes.Buffer
                        if err := writeMessageBody(&wire, []byte(payload)); err != nil {
                            t.Fatal(err)
                        }
                        if message, err := readMessage(bufio.NewReader(&wire)); err == nil {
                            t.Fatalf("readMessage accepted %q as %v", payload, message)
                        }
                    }
                }
 
                func TestPanickingCallbackIsAnsweredWithError(t *testing.T) {
                    c, host := newFakeHost(t)
                    var nilMap map[string]int
                    cases := []struct {
                        name     string
                        callback func(...any) any
                        want     string
                    }{
                        {"string", func(...any) any { panic("boom") }, "Callback execution failed: boom"},
                        {"error", func(...any) any { panic(errors.New("bad input")) }, "Callback execution failed: bad input"},
                        {"runtime error", func(...any) any { nilMap["k"] = 1; return nil }, "Callback execution failed: assignment to entry in nil map"},
                        {"Goexit", func(...any) any { runtime.Goexit(); return nil }, "Callback execution failed: goroutine exited without returning"},
                    }
                    for i, tc := range cases {
                        host.write(callbackRequest(i+1, c.registerCallback(tc.callback), nil))
                        response := host.read()
                        errObj, _ := response["error"].(map[string]any)
                        responseID, validID := jsonRPCID(response["id"])
                        if !validID || responseID != int64(i+1) || errObj == nil || response["result"] != nil {
                            t.Fatalf("%s: got %v, want an error response", tc.name, response)
                        }
                        code, validCode := jsonRPCID(errObj["code"])
                        if !validCode || code != -32603 || errObj["message"] != tc.want {
                            t.Errorf("%s: got error %v, want code -32603 and message %q", tc.name, errObj, tc.want)
                        }
                    }
 
                    healthy := c.registerCallback(func(...any) any { return "still alive" })
                    host.write(callbackRequest(100, healthy, nil))
                    if response := host.read(); response["result"] != "still alive" {
                        t.Fatalf("callback after a panic: got %v", response)
                    }
                }
 
                func TestCallbackWithMalformedHandleArgumentIsAnsweredWithError(t *testing.T) {
                    c, host := newFakeHost(t)
                    called := false
                    id := c.registerCallback(func(...any) any { called = true; return nil })
 
                    host.write(callbackRequest(1, id, map[string]any{"p0": map[string]any{"$handle": 1, "$type": nil}}))
                    response := host.read()
                    errObj, _ := response["error"].(map[string]any)
                    message, _ := errObj["message"].(string)
                    if !strings.Contains(message, "malformed handle") {
                        t.Fatalf("got %v, want a malformed handle error", response)
                    }
                    if called {
                        t.Fatal("the callback ran with a malformed argument")
                    }
                }
 
                func TestCallbackWithInvalidTypedArgumentIsNotCalled(t *testing.T) {
                    c, host := newFakeHost(t)
                    for _, args := range []any{nil, map[string]any{"p0": "not a number"}} {
                        called := false
                        id := c.registerCallback(func(args ...any) any {
                            value := callbackArg[int](args, 0)
                            called = true
                            return value
                        })
                        host.write(callbackRequest(1, id, args))
                        response := host.read()
                        errObj, _ := response["error"].(map[string]any)
                        code, validCode := jsonRPCID(errObj["code"])
                        if called || !validCode || code != -32602 {
                            t.Fatalf("got %v, called=%v; want invalid params without calling user code", response, called)
                        }
                    }
                }
 
                func TestTokenBearingCallbackAcceptsTokenIdAndOmittedNone(t *testing.T) {
                    for _, tc := range []struct {
                        name      string
                        args      any
                        wantToken bool
                    }{
                        {"token ID", map[string]any{"p0": "ct_callback"}, true},
                        {"CancellationToken.None", nil, false},
                    } {
                        t.Run(tc.name, func(t *testing.T) {
                            c, host := newFakeHost(t)
                            resource := newTestRedisResourceFromHandle(&handle{
                                HandleID: "redis",
                                TypeID:   "Aspire.Hosting.CodeGeneration.Go.Tests/TestRedisResource",
                            }, c)
                            received := make(chan *CancellationToken, 1)
                            done := make(chan TestRedisResource, 1)
                            go func() {
                                done <- resource.WithCancellableOperation(func(token *CancellationToken) {
                                    received <- token
                                })
                            }()
 
                            invocation := host.read()
                            params, _ := invocation["params"].([]any)
                            capabilityArgs, _ := params[1].(map[string]any)
                            callbackID, _ := capabilityArgs["operation"].(string)
                            if callbackID == "" {
                                t.Fatalf("missing callback ID in %v", invocation)
                            }
 
                            host.write(callbackRequest(2, callbackID, tc.args))
                            if response := host.read(); response["result"] == nil && response["error"] != nil {
                                t.Fatalf("callback failed: %v", response)
                            }
                            token := <-received
                            if (token != nil) != tc.wantToken {
                                t.Fatalf("got token %v, want token=%v", token, tc.wantToken)
                            }
                            if token != nil {
                                token.Cancel()
                                cancellation := host.read()
                                cancellationParams, _ := cancellation["params"].([]any)
                                if cancellation["method"] != "cancelToken" || len(cancellationParams) != 1 || cancellationParams[0] != "ct_callback" {
                                    t.Fatalf("got %v, want cancellation for the callback token", cancellation)
                                }
                                host.write(map[string]any{"jsonrpc": "2.0", "id": cancellation["id"], "result": true})
                            }
 
                            host.write(map[string]any{"jsonrpc": "2.0", "id": invocation["id"], "result": nil})
                            if result := <-done; result.Err() != nil {
                                t.Fatal(result.Err())
                            }
                        })
                    }
                }
 
                func TestUnencodableValuesDoNotDisconnect(t *testing.T) {
                    c, host := newFakeHost(t)
                    for _, value := range []any{math.NaN(), make(chan int)} {
                        if _, err := c.sendRequest(context.Background(), "bad", []any{value}); err == nil {
                            t.Fatal("unencodable request succeeded")
                        }
                        host.write(callbackRequest(1, c.registerCallback(func(...any) any { return value }), nil))
                        response := host.read()
                        errObj, _ := response["error"].(map[string]any)
                        code, validCode := jsonRPCID(errObj["code"])
                        if !validCode || code != -32603 {
                            t.Fatalf("got %v, want a callback encoding error", response)
                        }
                    }
                    done := make(chan error, 1)
                    go func() { _, err := c.ping(context.Background()); done <- err }()
                    request := host.read()
                    host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "result": "pong"})
                    if err := <-done; err != nil {
                        t.Fatal(err)
                    }
                }
 
                func TestWrapperErrorIsConcurrentAndFirstErrorWins(t *testing.T) {
                    wrapper := newHandleWrapperBase(&handle{HandleID: "h", TypeID: "Test/Type"}, nil)
                    first := errors.New("first")
                    wrapper.setErr(first)
                    var wg sync.WaitGroup
                    for range 100 {
                        wg.Add(1)
                        go func() {
                            defer wg.Done()
                            for range 100 {
                                wrapper.setErr(errors.New("later"))
                                if wrapper.Err() != first {
                                    t.Error("lost the original error")
                                }
                            }
                        }()
                    }
                    wg.Wait()
                }
 
                func TestCollectionToJSONKeepsResolutionError(t *testing.T) {
                    want := errors.New("getter failed")
                    parent := newErroredHandleWrapperBase(want, nil)
                    list := newListWithGetter[string](parent, "Test/getList")
                    dict := newDictWithGetter[string, string](parent, "Test/getDict")
                    if list.ToJSON() != nil || list.Err() != want {
                        t.Fatalf("list lost the error: %v", list.Err())
                    }
                    if dict.ToJSON() != nil || dict.Err() != want {
                        t.Fatalf("dictionary lost the error: %v", dict.Err())
                    }
                    // A failure first encountered by ToJSON must also reach Err,
                    // rather than only preserving an error already on the parent.
                    c := newClient("")
                    list = newListWithGetter[string](newHandleWrapperBase(&handle{}, c), "Test/getList")
                    dict = newDictWithGetter[string, string](newHandleWrapperBase(&handle{}, c), "Test/getDict")
                    if list.ToJSON() != nil || list.Err() == nil || list.Err().Error() != "not connected to AppHost" {
                        t.Fatalf("list swallowed its getter failure: %v", list.Err())
                    }
                    if dict.ToJSON() != nil || dict.Err() == nil || dict.Err().Error() != "not connected to AppHost" {
                        t.Fatalf("dictionary swallowed its getter failure: %v", dict.Err())
                    }
                }
 
                func TestRegisterCancellationIDsAreUnique(t *testing.T) {
                    c := newClient("")
                    const count = 5000
                    ids := make([]string, count)
                    var wg sync.WaitGroup
                    for i := range count {
                        wg.Add(1)
                        go func() {
                            defer wg.Done()
                            ids[i] = c.registerCancellation(NewCancellationToken())
                        }()
                    }
                    wg.Wait()
 
                    seen := make(map[string]bool, count)
                    for _, id := range ids {
                        if id == "" || seen[id] {
                            t.Fatalf("cancellation ID %q is empty or was handed out twice", id)
                        }
                        seen[id] = true
                    }
                }
 
                func TestCancellationNotifiesHostUntilCallReturns(t *testing.T) {
                    c, host := newFakeHost(t)
 
                    token := NewCancellationToken()
                    id := c.registerCancellation(token)
                    c.startCancellations(map[string]any{"cancellationToken": id})
                    token.Cancel()
                    request := host.read()
                    params, _ := request["params"].([]any)
                    if request["method"] != "cancelToken" || len(params) != 1 || params[0] != id {
                        t.Fatalf("got %v, want a cancelToken request for %s", request, id)
                    }
                    host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "result": true})
 
                    // Once the capability call that carried the ID has returned, canceling
                    // the token must no longer notify the host.
                    token = NewCancellationToken()
                    id = c.registerCancellation(token)
                    done := make(chan error, 1)
                    go func() {
                        _, err := c.invokeCapability(context.Background(), "Test/capability", map[string]any{"cancellationToken": id})
                        done <- err
                    }()
                    request = host.read()
                    if request["method"] != "invokeCapability" {
                        t.Fatalf("got %v, want an invokeCapability request", request)
                    }
                    host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "result": nil})
                    if err := <-done; err != nil {
                        t.Fatal(err)
                    }
 
                    c.cancellationsMu.Lock()
                    _, watching := c.cancellations[id]
                    c.cancellationsMu.Unlock()
                    if watching {
                        t.Fatal("the cancellation watcher outlived the call")
                    }
 
                    token.Cancel()
                    if err := host.peer.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
                        t.Fatal(err)
                    }
                    if msg, err := readMessage(host.reader); err == nil {
                        t.Fatalf("the host received %v after the call returned", msg)
                    }
                }
 
                func TestCancellationIsQueuedAfterItsInvocation(t *testing.T) {
                    c, host := newFakeHost(t)
                    token := NewCancellationToken()
                    id := c.registerCancellation(token)
                    token.Cancel()
                    done := make(chan error, 1)
                    go func() {
                        _, err := c.invokeCapability(context.Background(), "Test/capability", map[string]any{"cancellationToken": id})
                        done <- err
                    }()
                    request := host.read()
                    if request["method"] != "invokeCapability" {
                        t.Fatalf("cancellation overtook invocation: %v", request)
                    }
                    cancellation := host.read()
                    if cancellation["method"] != "cancelToken" {
                        t.Fatalf("expected cancellation after invocation, got %v", cancellation)
                    }
                    host.write(map[string]any{"jsonrpc": "2.0", "id": cancellation["id"], "result": true})
                    host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "result": nil})
                    if err := <-done; err != nil {
                        t.Fatal(err)
                    }
                }
 
                func TestUnansweredCancellationIsBounded(t *testing.T) {
                    c, host := newFakeHost(t)
                    done := make(chan bool, 1)
                    go func() { done <- c.cancelToken("ct_unanswered") }()
                    request := host.read()
                    if request["method"] != "cancelToken" {
                        t.Fatalf("unexpected request: %v", request)
                    }
                    select {
                    case result := <-done:
                        if result {
                            t.Fatal("unanswered cancellation succeeded")
                        }
                    case <-time.After(7 * time.Second):
                        t.Fatal("unanswered cancellation did not time out")
                    }
                    c.mu.Lock()
                    conn := c.conn
                    c.mu.Unlock()
                    conn.mu.Lock()
                    pending := len(conn.pending)
                    conn.mu.Unlock()
                    if pending != 0 {
                        t.Fatalf("%d unanswered cancellations still pending", pending)
                    }
                }
 
                func TestCancellationDuringCallReturnsNotifiesHostAndReleasesRegistration(t *testing.T) {
                    c, host := newFakeHost(t)
 
                    // Like generated code, every call uses its token's own context, and the host
                    // never answers the capability request, so only the cancellation can end a call.
                    // The returning call races the watcher that notifies the host and only some
                    // interleavings lose the notification, hence the repetitions.
                    for i := range 2000 {
                        token := NewCancellationToken()
                        id := c.registerCancellation(token)
                        done := make(chan error, 1)
                        go func() {
                            _, err := c.invokeCapability(token.Context(), "Test/capability", map[string]any{"cancellationToken": id})
                            done <- err
                        }()
                        request := host.read()
                        if request["method"] != "invokeCapability" {
                            t.Fatalf("pass %d: got %v, want an invokeCapability request", i, request)
                        }
 
                        token.Cancel()
                        select {
                        case err := <-done:
                            if !errors.Is(err, context.Canceled) {
                                t.Fatalf("pass %d: got %v, want context.Canceled", i, err)
                            }
                        case <-time.After(5 * time.Second):
                            t.Fatalf("pass %d: the call did not return after the token was canceled", i)
                        }
 
                        c.cancellationsMu.Lock()
                        registered := len(c.cancellations)
                        c.cancellationsMu.Unlock()
                        if registered != 0 {
                            t.Fatalf("pass %d: %d cancellation registrations outlived the call", i, registered)
                        }
 
                        if err := host.peer.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
                            t.Fatal(err)
                        }
                        request, err := readMessage(host.reader)
                        if err != nil {
                            t.Fatalf("pass %d: the host was not told about the cancellation: %v", i, err)
                        }
                        params, _ := request["params"].([]any)
                        if request["method"] != "cancelToken" || len(params) != 1 || params[0] != id {
                            t.Fatalf("pass %d: got %v, want a cancelToken request for %s", i, request, id)
                        }
                        host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "result": true})
                    }
                }
 
                func TestReadMessageRejectsInvalidContentLength(t *testing.T) {
                    for _, length := range []string{
                        strconv.Itoa(maxMessageSize + 1),
                        "9223372036854775807",
                        "9223372036854775808",
                        "-1",
                        "0",
                        "abc",
                        "",
                    } {
                        var before, after runtime.MemStats
                        runtime.ReadMemStats(&before)
                        _, err := readMessage(bufio.NewReader(strings.NewReader("Content-Length: " + length + "\r\n\r\n{}")))
                        runtime.ReadMemStats(&after)
                        if err == nil {
                            t.Errorf("Content-Length %q: expected an error", length)
                        }
                        if grown := after.TotalAlloc - before.TotalAlloc; grown > 1<<20 {
                            t.Errorf("Content-Length %q: allocated %d bytes before rejecting the frame", length, grown)
                        }
                    }
 
                    var frame bytes.Buffer
                    if err := writeMessage(&frame, map[string]any{"jsonrpc": "2.0", "id": 1, "result": "ok"}); err != nil {
                        t.Fatal(err)
                    }
                    msg, err := readMessage(bufio.NewReader(&frame))
                    if err != nil || msg["result"] != "ok" {
                        t.Fatalf("a valid frame: got %v, %v", msg, err)
                    }
                }
 
                func TestReadMessageBoundsHeaders(t *testing.T) {
                    for name, tc := range map[string]struct{ headers, want string }{
                        "unterminated line": {strings.Repeat("x", maxHeaderSize + 1), "AppHost message headers exceed 8192 bytes"},
                        "too many headers": {strings.Repeat("X: y\r\n", maxHeaderCount + 1), "AppHost message has more than 16 headers"},
                        "total size": {strings.Repeat("X: " + strings.Repeat("y", 1020) + "\r\n", 9), "AppHost message headers exceed 8192 bytes"},
                    } {
                        if _, err := readMessage(bufio.NewReader(strings.NewReader(tc.headers + "\r\n{}"))); err == nil || err.Error() != tc.want {
                            t.Errorf("%s: got %v, want %q", name, err, tc.want)
                        }
                    }
                    // A long but valid line may span several reader buffers.
                    headers := "X: " + strings.Repeat("y", 7000) + "\r\nContent-Length: 2\r\n\r\n{}"
                    if _, err := readMessage(bufio.NewReaderSize(strings.NewReader(headers), 32)); err != nil {
                        t.Fatalf("valid bounded headers rejected: %v", err)
                    }
                }
 
                func TestWrapIfHandleRejectsMalformedHandle(t *testing.T) {
                    c := newClient("")
                    malformed := map[string]any{"$handle": float64(1), "$type": nil}
                    for name, value := range map[string]any{
                        "direct":   malformed,
                        "in slice": []any{malformed},
                        "in map":   map[string]any{"nested": malformed},
                    } {
                        if _, err := wrapIfHandle(value, c); err == nil {
                            t.Errorf("%s: expected an error", name)
                        }
                    }
 
                    wrapped, err := wrapIfHandle(map[string]any{"$handle": "h1", "$type": "Test/Type"}, c)
                    if h, ok := wrapped.(*handle); err != nil || !ok || h.HandleID != "h1" || h.TypeID != "Test/Type" {
                        t.Fatalf("a valid handle: got %v, %v", wrapped, err)
                    }
                }
 
                func TestExtractResultKeepsErrorCodeAndData(t *testing.T) {
                    // Decode through the wire format so the code arrives as float64, as it does from the host.
                    var frame bytes.Buffer
                    if err := writeMessage(&frame, map[string]any{
                        "jsonrpc": "2.0",
                        "id":      1,
                        "error":   map[string]any{"code": -32601, "message": "Method not found", "data": map[string]any{"method": "nope"}},
                    }); err != nil {
                        t.Fatal(err)
                    }
                    response, err := readMessage(bufio.NewReader(&frame))
                    if err != nil {
                        t.Fatal(err)
                    }
 
                    _, err = extractResult(response)
                    if err == nil || err.Error() != "Method not found" {
                        t.Fatalf("got %v, want the message alone", err)
                    }
                    var coded interface{ Code() int }
                    if !errors.As(err, &coded) || coded.Code() != -32601 {
                        t.Fatalf("the code was not preserved: %v", err)
                    }
                    var withData interface{ Data() any }
                    if !errors.As(err, &withData) {
                        t.Fatalf("the data was not preserved: %v", err)
                    }
                    if data, _ := withData.Data().(map[string]any); data["method"] != "nope" {
                        t.Fatalf("got data %v", withData.Data())
                    }
 
                    cases := []struct {
                        name     string
                        response map[string]any
                        message  string
                        code     int
                    }{
                        {"code from connection.close", map[string]any{"error": map[string]any{"code": -32000, "message": "connection closed"}}, "connection closed", -32000},
                        {"missing message", map[string]any{"error": map[string]any{"code": float64(-32603)}}, "AppHost returned a JSON-RPC error (code -32603)", -32603},
                        {"error is not an object", map[string]any{"error": "oops"}, "AppHost returned a JSON-RPC error (code 0)", 0},
                    }
                    for _, tc := range cases {
                        _, err := extractResult(tc.response)
                        if err == nil || err.Error() != tc.message {
                            t.Errorf("%s: got %v, want %q", tc.name, err, tc.message)
                        }
                        if !errors.As(err, &coded) || coded.Code() != tc.code {
                            t.Errorf("%s: got code %v, want %d", tc.name, coded, tc.code)
                        }
                    }
 
                    if result, err := extractResult(map[string]any{"result": "ok"}); err != nil || result != "ok" {
                        t.Errorf("success response: got %v, %v", result, err)
                    }
                }
 
                func TestRequestErrorKeepsHostErrorCode(t *testing.T) {
                    c, host := newFakeHost(t)
                    done := make(chan error, 1)
                    go func() {
                        _, err := c.ping(context.Background())
                        done <- err
                    }()
                    request := host.read()
                    host.write(map[string]any{"jsonrpc": "2.0", "id": request["id"], "error": map[string]any{"code": -32601, "message": "Method not found"}})
 
                    err := <-done
                    var coded interface{ Code() int }
                    if !errors.As(err, &coded) || coded.Code() != -32601 {
                        t.Fatalf("got %v, want an error carrying code -32601", err)
                    }
                }
                """, timeout.Token);
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "decode_test.go"), """
                package aspire
 
                import (
                    "encoding/json"
                    "reflect"
                    "strconv"
                    "strings"
                    "testing"
                )
 
                const decodeTestTypeID = "Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource"
 
                // decodeTestResource stands in for a generated handle interface such as Resource.
                type decodeTestResource interface {
                    handleReference
                }
 
                // decodeTestWrapper stands in for the generated wrapper registered for decodeTestTypeID.
                type decodeTestWrapper struct{ *resourceBuilderBase }
 
                type decodeTestKey string
 
                type decodeTestDto struct {
                    Name  string `json:"name"`
                    Count int    `json:"count"`
                }
 
                func newDecodeTestClient() *client {
                    c := newClient("")
                    c.registerHandleWrapper(decodeTestTypeID, func(h *handle, c *client) any {
                        return &decodeTestWrapper{newResourceBuilderBase(h, c)}
                    })
                    return c
                }
 
                func decodeTestHandle(id string) string {
                    return `{"$handle":"` + id + `","$type":"` + decodeTestTypeID + `"}`
                }
 
                func decodeTestHandles(ids ...string) string {
                    handles := make([]string, len(ids))
                    for i, id := range ids {
                        handles[i] = decodeTestHandle(id)
                    }
                    return "[" + strings.Join(handles, ",") + "]"
                }
 
                // decodeTestIDs lists the handle ids of items in order, with <nil> for a nil item.
                func decodeTestIDs(items []decodeTestResource) string {
                    ids := make([]string, len(items))
                    for i, item := range items {
                        if item == nil {
                            ids[i] = "<nil>"
                            continue
                        }
                        ids[i] = item.getHandle().HandleID
                    }
                    return strings.Join(ids, ",")
                }
 
                // decodeWire takes a JSON-RPC result through the steps a capability result takes:
                // parse it, wrap its handles, decode it into T.
                func decodeWire[T any](t *testing.T, c *client, payload string) (T, error) {
                    t.Helper()
                    var raw any
                    if err := json.Unmarshal([]byte(payload), &raw); err != nil {
                        t.Fatal(err)
                    }
                    wrapped, err := wrapIfHandle(raw, c)
                    if err != nil {
                        t.Fatal(err)
                    }
                    return decodeAs[T](wrapped)
                }
 
                func decodeTestExpectError(t *testing.T, name string, err error, want ...string) {
                    t.Helper()
                    if err == nil {
                        t.Errorf("%s: got no error, want one mentioning %q", name, want)
                        return
                    }
                    for _, fragment := range want {
                        if !strings.Contains(err.Error(), fragment) {
                            t.Errorf("%s: error %q does not mention %q", name, err, fragment)
                        }
                    }
                }
 
                func TestDecodeListOfHandles(t *testing.T) {
                    c := newDecodeTestClient()
                    cases := []struct{ name, payload, ids string }{
                        {"one handle", decodeTestHandles("1"), "1"},
                        {"several handles", decodeTestHandles("1", "2", "3"), "1,2,3"},
                        {"null element", "[" + decodeTestHandle("1") + ",null]", "1,<nil>"},
                    }
                    for _, tc := range cases {
                        got, err := decodeWire[[]decodeTestResource](t, c, tc.payload)
                        if err != nil {
                            t.Errorf("%s: %v", tc.name, err)
                            continue
                        }
                        if ids := decodeTestIDs(got); ids != tc.ids {
                            t.Errorf("%s: got handles %s, want %s", tc.name, ids, tc.ids)
                        }
                        if _, ok := got[0].(*decodeTestWrapper); !ok {
                            t.Errorf("%s: first element is %T, want the registered wrapper", tc.name, got[0])
                        }
                    }
                }
 
                func TestDecodeListsOfValues(t *testing.T) {
                    c := newDecodeTestClient()
 
                    dtos, err := decodeWire[[]decodeTestDto](t, c, `[{"name":"a","count":1},{"name":"b","count":2}]`)
                    if err != nil || !reflect.DeepEqual(dtos, []decodeTestDto{{"a", 1}, {"b", 2}}) {
                        t.Errorf("DTOs: got %v, %v", dtos, err)
                    }
 
                    pointers, err := decodeWire[[]*decodeTestDto](t, c, `[{"name":"a","count":1},null]`)
                    if err != nil || !reflect.DeepEqual(pointers, []*decodeTestDto{{"a", 1}, nil}) {
                        t.Errorf("DTO pointers: got %v, %v", pointers, err)
                    }
 
                    names, err := decodeWire[[]string](t, c, `["a","b"]`)
                    if err != nil || !reflect.DeepEqual(names, []string{"a", "b"}) {
                        t.Errorf("strings: got %v, %v", names, err)
                    }
 
                    numbers, err := decodeWire[[]int](t, c, `[1,2,3]`)
                    if err != nil || !reflect.DeepEqual(numbers, []int{1, 2, 3}) {
                        t.Errorf("ints: got %v, %v", numbers, err)
                    }
 
                    floats, err := decodeWire[[]float64](t, c, `[1.5,2]`)
                    if err != nil || !reflect.DeepEqual(floats, []float64{1.5, 2}) {
                        t.Errorf("floats: got %v, %v", floats, err)
                    }
 
                    mixed, err := decodeWire[[]any](t, c, "["+decodeTestHandle("1")+`,{"name":"a"},7,"x",null]`)
                    if err != nil || len(mixed) != 5 {
                        t.Fatalf("mixed list: got %v, %v", mixed, err)
                    }
                    if _, ok := mixed[0].(*decodeTestWrapper); !ok {
                        t.Errorf("mixed list: first element is %T, want the registered wrapper", mixed[0])
                    }
                    if want := []any{map[string]any{"name": "a"}, float64(7), "x", nil}; !reflect.DeepEqual(mixed[1:], want) {
                        t.Errorf("mixed list: got %v, want %v", mixed[1:], want)
                    }
                }
 
                func TestDecodeEmptyList(t *testing.T) {
                    c := newDecodeTestClient()
 
                    list, err := decodeWire[[]decodeTestResource](t, c, `[]`)
                    if err != nil || list == nil || len(list) != 0 {
                        t.Errorf("empty list: got %#v, %v, want an empty slice", list, err)
                    }
 
                    entries, err := decodeWire[map[string]decodeTestResource](t, c, `{}`)
                    if err != nil || entries == nil || len(entries) != 0 {
                        t.Errorf("empty map: got %#v, %v, want an empty map", entries, err)
                    }
                }
 
                func TestDecodeNilResult(t *testing.T) {
                    if got, err := decodeAs[[]decodeTestResource](nil); got != nil || err != nil {
                        t.Errorf("nil list: got %#v, %v, want nil", got, err)
                    }
                    if got, err := decodeAs[map[string]decodeTestResource](nil); got != nil || err != nil {
                        t.Errorf("nil map: got %#v, %v, want nil", got, err)
                    }
 
                    // Typed nils are not nil interfaces, so they reach the element loop and must not panic.
                    if got, err := decodeAs[[]decodeTestResource]([]any(nil)); err != nil || len(got) != 0 {
                        t.Errorf("nil []any: got %#v, %v, want an empty list", got, err)
                    }
                    if got, err := decodeAs[map[string]decodeTestResource](map[string]any(nil)); err != nil || len(got) != 0 {
                        t.Errorf("nil map[string]any: got %#v, %v, want an empty map", got, err)
                    }
                    if got, err := decodeAs[[]decodeTestResource]([]any{(*decodeTestWrapper)(nil)}); err != nil || len(got) != 1 {
                        t.Errorf("nil wrapper element: got %#v, %v, want one element", got, err)
                    }
                }
 
                func TestDecodeMapOfHandles(t *testing.T) {
                    c := newDecodeTestClient()
                    payload := `{"a":` + decodeTestHandle("1") + `,"b":` + decodeTestHandle("2") + `}`
 
                    got, err := decodeWire[map[string]decodeTestResource](t, c, payload)
                    if err != nil || len(got) != 2 {
                        t.Fatalf("map of handles: got %v, %v", got, err)
                    }
                    for key, id := range map[string]string{"a": "1", "b": "2"} {
                        if item, ok := got[key]; !ok || item.getHandle().HandleID != id {
                            t.Errorf("map of handles: got %v for %q, want handle %s", item, key, id)
                        }
                    }
 
                    named, err := decodeWire[map[decodeTestKey]decodeTestResource](t, c, payload)
                    if err != nil || len(named) != 2 || named["a"] == nil || named["b"] == nil {
                        t.Errorf("map with a named key type: got %v, %v", named, err)
                    }
 
                    dtos, err := decodeWire[map[string]decodeTestDto](t, c, `{"x":{"name":"a","count":1}}`)
                    if err != nil || !reflect.DeepEqual(dtos, map[string]decodeTestDto{"x": {"a", 1}}) {
                        t.Errorf("map of DTOs: got %v, %v", dtos, err)
                    }
 
                    counts, err := decodeWire[map[string]int](t, c, `{"x":1,"y":2}`)
                    if err != nil || !reflect.DeepEqual(counts, map[string]int{"x": 1, "y": 2}) {
                        t.Errorf("map of ints: got %v, %v", counts, err)
                    }
                }
 
                func TestDecodeNestedLists(t *testing.T) {
                    c := newDecodeTestClient()
 
                    nested, err := decodeWire[[][]decodeTestResource](t, c, "["+decodeTestHandles("1", "2")+",[],"+decodeTestHandles("3")+"]")
                    if err != nil || len(nested) != 3 || decodeTestIDs(nested[0]) != "1,2" || len(nested[1]) != 0 || decodeTestIDs(nested[2]) != "3" {
                        t.Errorf("list of lists: got %v, %v", nested, err)
                    }
 
                    grouped, err := decodeWire[map[string][]decodeTestResource](t, c, `{"x":`+decodeTestHandles("1", "2")+`}`)
                    if err != nil || decodeTestIDs(grouped["x"]) != "1,2" {
                        t.Errorf("map of lists: got %v, %v", grouped, err)
                    }
 
                    maps, err := decodeWire[[]map[string]decodeTestResource](t, c, `[{"a":`+decodeTestHandle("1")+`}]`)
                    if err != nil || len(maps) != 1 || maps[0]["a"] == nil {
                        t.Errorf("list of maps: got %v, %v", maps, err)
                    }
                }
 
                func TestDecodeNamesTheElementThatFailed(t *testing.T) {
                    c := newDecodeTestClient()
                    unregistered := `{"$handle":"9","$type":"Aspire.Hosting/Aspire.Hosting.Unregistered"}`
 
                    _, err := decodeWire[[]decodeTestResource](t, c, "["+decodeTestHandle("1")+",7]")
                    decodeTestExpectError(t, "number in a handle list", err, "element 1 of []aspire.decodeTestResource")
 
                    _, err = decodeWire[[]decodeTestResource](t, c, "["+decodeTestHandle("1")+","+unregistered+"]")
                    decodeTestExpectError(t, "handle without a wrapper", err, "element 1 of []aspire.decodeTestResource")
 
                    _, err = decodeWire[[]string](t, c, `["a","b",3]`)
                    decodeTestExpectError(t, "number in a string list", err, "element 2 of []string")
 
                    _, err = decodeWire[map[string]decodeTestResource](t, c, `{"a":`+decodeTestHandle("1")+`,"b":7}`)
                    decodeTestExpectError(t, "number in a handle map", err, `entry "b" of map[string]aspire.decodeTestResource`)
 
                    _, err = decodeWire[[][]decodeTestResource](t, c, "["+decodeTestHandles("1")+",[7]]")
                    decodeTestExpectError(t, "number in a nested list", err, "element 1 of [][]aspire.decodeTestResource", "element 0 of []aspire.decodeTestResource")
 
                    _, err = decodeWire[[][]string](t, c, `[[["a"]]]`)
                    decodeTestExpectError(t, "data nested deeper than the type", err, "element 0 of [][]string")
                }
 
                func TestDecodeSingleValuesAreUnchanged(t *testing.T) {
                    c := newDecodeTestClient()
 
                    resource, err := decodeWire[decodeTestResource](t, c, decodeTestHandle("1"))
                    if err != nil || resource == nil || resource.getHandle().HandleID != "1" {
                        t.Errorf("single handle: got %v, %v", resource, err)
                    }
 
                    dto, err := decodeWire[decodeTestDto](t, c, `{"name":"a","count":1}`)
                    if err != nil || dto != (decodeTestDto{"a", 1}) {
                        t.Errorf("DTO: got %v, %v", dto, err)
                    }
 
                    pointer, err := decodeWire[*decodeTestDto](t, c, `{"name":"a","count":1}`)
                    if err != nil || pointer == nil || *pointer != (decodeTestDto{"a", 1}) {
                        t.Errorf("DTO pointer: got %v, %v", pointer, err)
                    }
 
                    // A malformed DTO must fail rather than silently substitute zero-valued fields.
                    if _, err := decodeWire[decodeTestDto](t, c, `{"name":5,"count":3}`); err == nil {
                        t.Error("a DTO with a bad field decoded without an error")
                    }
 
                    // []byte arrives as a base64 string and a map with integer keys is not a string-keyed
                    // map, so both keep the JSON round trip.
                    data, err := decodeWire[[]byte](t, c, `"aGk="`)
                    if err != nil || string(data) != "hi" {
                        t.Errorf("bytes: got %q, %v", data, err)
                    }
                    byNumber, err := decodeWire[map[int]string](t, c, `{"1":"a"}`)
                    if err != nil || !reflect.DeepEqual(byNumber, map[int]string{1: "a"}) {
                        t.Errorf("map with integer keys: got %v, %v", byNumber, err)
                    }
 
                    if _, err := decodeWire[[]string](t, c, `"oops"`); err == nil {
                        t.Error("a string decoded into a list without an error")
                    }
                    if _, err := decodeWire[map[string]string](t, c, `["a"]`); err == nil {
                        t.Error("a list decoded into a map without an error")
                    }
                }
 
                func TestDecodeListOfHandlesDoesNotRoundTripThroughJSON(t *testing.T) {
                    c := newDecodeTestClient()
                    ids := make([]string, 100)
                    for i := range ids {
                        ids[i] = strconv.Itoa(i)
                    }
                    var raw any
                    if err := json.Unmarshal([]byte(decodeTestHandles(ids...)), &raw); err != nil {
                        t.Fatal(err)
                    }
                    wrapped, err := wrapIfHandle(raw, c)
                    if err != nil {
                        t.Fatal(err)
                    }
 
                    allocs := testing.AllocsPerRun(10, func() {
                        if _, err := decodeAs[[]decodeTestResource](wrapped); err != nil {
                            t.Error(err)
                        }
                    })
                    // Wrappers are assigned as they are, so the result slice is the only allocation that
                    // grows with the list. Re-encoding each element would take hundreds.
                    if allocs > 10 {
                        t.Errorf("decoding 100 handles allocated %v times, want a handful", allocs)
                    }
                }
                """, timeout.Token);
 
            var startInfo = new ProcessStartInfo("go")
            {
                WorkingDirectory = directory.FullName,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                ArgumentList = { "test", "-timeout", "30s", "." }
            };
            EnableGoRaceDetection(startInfo);
            using var process = Process.Start(startInfo)!;
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go runtime hardening test failed:{Environment.NewLine}{await stdout}{await stderr}");
            }
            finally
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedRuntime_PassesGoTests()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        try
        {
            var files = _generator.GenerateDistributedApplication(CreateContextFromTestAssembly());
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "runtime_test.go"), """
                package aspire
 
                import (
                    "bufio"
                    "bytes"
                    "encoding/json"
                    "errors"
                    "fmt"
                    "net"
                    "reflect"
                    "slices"
                    "strconv"
                    "strings"
                    "sync"
                    "testing"
                )
 
                func TestCallbackAndMainCanChainOnCapturedBuilder(t *testing.T) {
                    c := newClient("")
                    resource := newTestRedisResourceFromHandle(&handle{HandleID: "h", TypeID: "Test/Redis"}, c)
                    callback := c.registerCallback(func(...any) any {
                        resource.WithPersistence()
                        resource.GetTags()
                        return nil
                    })
                    var wg sync.WaitGroup
                    wg.Add(1)
                    go func() {
                        defer wg.Done()
                        for range 1000 {
                            if _, err := c.invokeCallback(callback, nil); err != nil {
                                t.Error(err)
                            }
                        }
                    }()
                    for range 1000 {
                        resource.WithPersistence()
                        resource.GetTags()
                        resource.Err()
                    }
                    wg.Wait()
                    if resource.Err() == nil {
                        t.Fatal("the disconnected capability error was lost")
                    }
                }
 
                func TestDeepUpdateKeepsCallback(t *testing.T) {
                    calls := 0
                    merged := deepUpdate(&WithOptionalCallbackOptions{}, &WithOptionalCallbackOptions{
                        Callback: func(TestCallbackContext) { calls++ },
                    })
                    if merged.Callback == nil {
                        t.Fatal("callback was dropped")
                    }
                    merged.Callback(nil)
                    if calls != 1 {
                        t.Fatalf("expected the callback to run once, ran %d times", calls)
                    }
                }
 
                func TestDeepUpdateAcceptsNilCallback(t *testing.T) {
                    merged := deepUpdate(&WithOptionalCallbackOptions{}, &WithOptionalCallbackOptions{})
                    if merged.Callback != nil {
                        t.Fatal("unexpected callback")
                    }
                }
 
                func TestDeepUpdateKeepsCancellationToken(t *testing.T) {
                    token := NewCancellationToken()
                    merged := deepUpdate(&GetStatusAsyncOptions{}, &GetStatusAsyncOptions{CancellationToken: token})
                    if merged.CancellationToken != token {
                        t.Fatal("cancellation token was replaced")
                    }
                }
 
                func TestDeepUpdateMergesNestedValuesWithoutModifyingInputs(t *testing.T) {
                    dst := &TestNestedDto{Id: "dst", Config: &TestConfigDto{Name: "dst"}, Counts: map[string]int{"a": 1, "b": 2}}
                    src := &TestNestedDto{Id: "src", Config: &TestConfigDto{Name: "src"}, Counts: map[string]int{"b": 3, "c": 4}}
 
                    merged := deepUpdate(dst, src)
 
                    want := &TestNestedDto{Id: "src", Config: &TestConfigDto{Name: "src"}, Counts: map[string]int{"a": 1, "b": 3, "c": 4}}
                    if !reflect.DeepEqual(merged, want) {
                        t.Fatalf("got %+v, want %+v", merged, want)
                    }
                    if len(dst.Counts) != 2 || len(src.Counts) != 2 || dst.Id != "dst" || dst.Config.Name != "dst" {
                        t.Fatal("merge modified its inputs")
                    }
                }
 
                type namedHandle interface{ Name() string }
 
                type namedValue string
 
                func (n namedValue) Name() string { return string(n) }
 
                // workDto and nestedCallbackOptions mirror generated DTOs with callback
                // properties and optional handle parameters, which the test types do not have.
                type workDto struct {
                    Label *string `json:"label,omitempty"`
                    Work  func()  `json:"work,omitempty"`
                }
 
                type nestedCallbackOptions struct {
                    Progress *workDto     `json:"progress,omitempty"`
                    Handle   *namedHandle `json:"handle,omitempty"`
                }
 
                func TestDeepUpdateKeepsCallbacksAndHandlesInNestedValues(t *testing.T) {
                    label := "step"
                    ran := false
                    var handle namedHandle = namedValue("resource")
                    src := &nestedCallbackOptions{
                        Progress: &workDto{Label: &label, Work: func() { ran = true }},
                        Handle:   &handle,
                    }
 
                    merged := deepUpdate(&nestedCallbackOptions{Progress: &workDto{}}, src)
 
                    merged.Progress.Work()
                    if !ran || *merged.Progress.Label != label || (*merged.Handle).Name() != "resource" {
                        t.Fatalf("nested callback or handle was lost: %+v", merged)
                    }
                }
 
                // The client has no connection, so a value that passes the union guard fails later with
                // a transport error, while a value that does not pass it fails with the guard's error.
                func scalarFieldError(value any) error {
                    resource := newTestRedisResourceFromHandle(&handle{HandleID: "1", TypeID: "type"}, newClient(""))
                    resource.WithScalarField("spec.maxReplicaCount", value)
                    return resource.Err()
                }
 
                func TestUnionGuardAcceptsEveryGoNumericType(t *testing.T) {
                    values := []any{
                        "text", true, 1.5, float32(1.5), 3, int8(3), int16(3), int32(3), int64(3),
                        uint(3), uint8(3), uint16(3), uint32(3), uint64(3),
                    }
                    for _, value := range values {
                        err := scalarFieldError(value)
                        if err == nil || !strings.Contains(err.Error(), "not connected to AppHost") {
                            t.Errorf("%T: got %v, want the transport error", value, err)
                        }
                    }
                }
 
                func TestUnionGuardAcceptsPreservedJSONNumber(t *testing.T) {
                    var wire bytes.Buffer
                    if err := writeMessageBody(&wire, []byte(`{"jsonrpc":"2.0","id":1,"result":9007199254740993}`)); err != nil {
                        t.Fatal(err)
                    }
                    message, err := readMessage(bufio.NewReader(&wire))
                    if err != nil {
                        t.Fatal(err)
                    }
                    value, ok := message["result"].(json.Number)
                    if !ok {
                        t.Fatalf("result is %T, want json.Number", message["result"])
                    }
                    if err := scalarFieldError(value); err == nil || !strings.Contains(err.Error(), "not connected to AppHost") {
                        t.Fatalf("got %v, want the transport error", err)
                    }
 
                    if err := scalarFieldError(json.Number("NaN")); err == nil || !strings.Contains(err.Error(), "invalid json.Number") {
                        t.Fatalf("got %v, want an invalid json.Number error", err)
                    }
                }
 
                func TestUnionGuardRejectsOtherTypes(t *testing.T) {
                    for _, value := range []any{nil, []string{"a"}, map[string]any{"a": 1}} {
                        err := scalarFieldError(value)
                        if err == nil || !strings.Contains(err.Error(), "must be one of [string, float64, bool]") {
                            t.Errorf("%T: got %v, want the guard error", value, err)
                        }
                    }
                }
 
                const (
                    testRedisTypeID   = "Aspire.Hosting.CodeGeneration.Go.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestRedisResource"
                    applicationTypeID = "application"
                )
 
                // connectChildErrorHost connects a client to an in-process AppHost that answers
                // every capability call with a new handle, or with an error when the method (the
                // part of the capability ID after the last "/") is in failing. The returned
                // function lists the methods the AppHost has received, in order.
                func connectChildErrorHost(t *testing.T, failing ...string) (*client, func() []string) {
                    t.Helper()
 
                    clientEnd, serverEnd := net.Pipe()
                    c := newClient("")
                    conn := newConnection(clientEnd, c, c.onConnectionClose)
                    c.mu.Lock()
                    c.conn = conn
                    c.mu.Unlock()
                    conn.start()
                    registerWrappers(c)
                    c.registerHandleWrapper(applicationTypeID, func(h *handle, c *client) any {
                        return &distributedApplication{resourceBuilderBase: newResourceBuilderBase(h, c)}
                    })
                    t.Cleanup(func() {
                        c.disconnect()
                        serverEnd.Close()
                    })
 
                    var mu sync.Mutex
                    var received []string
                    go func() {
                        reader := bufio.NewReader(serverEnd)
                        for n := 1; ; n++ {
                            request, err := readMessage(reader)
                            if err != nil {
                                return
                            }
                            capability := request["params"].([]any)[0].(string)
                            method := capability[strings.LastIndex(capability, "/")+1:]
                            mu.Lock()
                            received = append(received, method)
                            mu.Unlock()
 
                            response := map[string]any{"jsonrpc": "2.0", "id": request["id"]}
                            switch {
                            case slices.Contains(failing, method):
                                response["error"] = map[string]any{"message": fmt.Sprintf("%s failed (request %d)", method, n)}
                            case method == "build":
                                response["result"] = map[string]any{"$handle": "application", "$type": applicationTypeID}
                            default:
                                response["result"] = map[string]any{"$handle": strconv.Itoa(n), "$type": testRedisTypeID}
                            }
                            if writeMessage(serverEnd, response) != nil {
                                return
                            }
                        }
                    }()
 
                    return c, func() []string {
                        mu.Lock()
                        defer mu.Unlock()
                        return slices.Clone(received)
                    }
                }
 
                func newBuilderOn(c *client) IDistributedApplicationBuilder {
                    return newIDistributedApplicationBuilderFromHandle(&handle{HandleID: "0", TypeID: "builder"}, c)
                }
 
                func TestFailedChildErrorReachesBuilder(t *testing.T) {
                    c, received := connectChildErrorHost(t, "addTestRedis")
                    builder := newBuilderOn(c)
 
                    cache := builder.AddTestRedis("cache")
                    if cache.Err() == nil {
                        t.Fatal("the failed AddTestRedis left no error on the child")
                    }
                    if !errors.Is(builder.Err(), cache.Err()) {
                        t.Errorf("builder.Err() = %v, want the child's error %v", builder.Err(), cache.Err())
                    }
 
                    app, err := builder.Build()
                    if app != nil || !errors.Is(err, cache.Err()) {
                        t.Errorf("Build() = %v, %v; want nil and the child's error %v", app, err, cache.Err())
                    }
                    if got, want := received(), []string{"addTestRedis"}; !slices.Equal(got, want) {
                        t.Errorf("the AppHost received %v, want %v (Build must not be sent)", got, want)
                    }
                }
 
                func TestAllRecordedChildErrorsReachBuilder(t *testing.T) {
                    c, received := connectChildErrorHost(t, "addTestRedis")
                    builder := newBuilderOn(c)
 
                    first := builder.AddTestRedis("first")
                    second := builder.AddTestRedis("second")
 
                    got := builder.Err()
                    if !errors.Is(got, first.Err()) || !errors.Is(got, second.Err()) {
                        t.Errorf("builder.Err() = %v, want both %v and %v", got, first.Err(), second.Err())
                    }
                    if want := first.Err().Error() + "\n" + second.Err().Error(); got.Error() != want {
                        t.Errorf("builder.Err() = %q, want %q", got, want)
                    }
                    if _, err := builder.Build(); !errors.Is(err, first.Err()) || !errors.Is(err, second.Err()) {
                        t.Errorf("Build() error = %v, want both %v and %v", err, first.Err(), second.Err())
                    }
                    if got, want := received(), []string{"addTestRedis", "addTestRedis"}; !slices.Equal(got, want) {
                        t.Errorf("the AppHost received %v, want %v (Build must not be sent)", got, want)
                    }
                }
 
                func TestFormatErrorIncludesEveryJoinedCapabilityError(t *testing.T) {
                    first := &CapabilityError{err: atsError{
                        Code:       "FIRST",
                        Message:    "first failed",
                        Capability: "addFirst",
                    }}
                    second := &CapabilityError{err: atsError{
                        Code:       "SECOND",
                        Message:    "second failed",
                        Capability: "addSecond",
                    }}
 
                    got := FormatError(errors.Join(first, second))
                    want := "Capability Error: first failed\n" +
                        "  Code: FIRST\n" +
                        "  Capability: addFirst\n" +
                        "Capability Error: second failed\n" +
                        "  Code: SECOND\n" +
                        "  Capability: addSecond"
                    if got != want {
                        t.Errorf("FormatError() = %q, want %q", got, want)
                    }
                }
 
                func TestFormatErrorIncludesJoinedCapabilityAndOrdinaryErrors(t *testing.T) {
                    capability := &CapabilityError{err: atsError{
                        Code:       "CAPABILITY",
                        Message:    "capability failed",
                        Capability: "addResource",
                    }}
                    ordinary := errors.New("ordinary failure")
 
                    got := FormatError(errors.Join(capability, ordinary))
                    want := "Capability Error: capability failed\n" +
                        "  Code: CAPABILITY\n" +
                        "  Capability: addResource\n" +
                        "ordinary failure"
                    if got != want {
                        t.Errorf("FormatError() = %q, want %q", got, want)
                    }
                }
 
                func TestFormatErrorPreservesOrdinaryMultiWrappedError(t *testing.T) {
                    resourceErr := errors.New("resource failure")
                    cleanupErr := errors.New("cleanup failure")
                    joined := fmt.Errorf("deployment failed for cache: %w; cleanup failed: %w", resourceErr, cleanupErr)
 
                    if got := FormatError(joined); got != joined.Error() {
                        t.Errorf("FormatError() = %q, want %q", got, joined.Error())
                    }
                }
 
                func TestPropagatedChildErrorIsNotRecordedAgain(t *testing.T) {
                    c := newClient("")
                    builder := newBuilderOn(c)
 
                    failure := fmt.Errorf("child failed")
                    newErroredHandleWrapperBase(failure, c)
                    newPropagatedErroredResourceBuilder(failure, c)
                    newPropagatedErroredResourceBuilder(failure, c)
 
                    if got := builder.Err(); !errors.Is(got, failure) || got.Error() != failure.Error() {
                        t.Errorf("builder.Err() = %v, want the original error %v", got, failure)
                    }
                }
 
                func TestFailedWithCallOnChildReachesBuilder(t *testing.T) {
                    c, _ := connectChildErrorHost(t, "withConnectionStringDirect")
                    builder := newBuilderOn(c)
 
                    cache := builder.AddTestRedis("cache")
                    if builder.Err() != nil {
                        t.Fatalf("builder.Err() = %v before any call failed", builder.Err())
                    }
 
                    cache.WithConnectionStringDirect("connection")
                    if cache.Err() == nil || !errors.Is(builder.Err(), cache.Err()) {
                        t.Errorf("builder.Err() = %v, want the child's error %v", builder.Err(), cache.Err())
                    }
                }
 
                func TestBuildSucceedsWhenNoCallFailed(t *testing.T) {
                    c, received := connectChildErrorHost(t)
                    builder := newBuilderOn(c)
 
                    builder.AddTestRedis("cache").WithConnectionStringDirect("connection")
                    if builder.Err() != nil {
                        t.Fatalf("builder.Err() = %v, want nil", builder.Err())
                    }
                    if _, err := builder.Build(); err != nil {
                        t.Fatalf("Build() error = %v", err)
                    }
                    if got, want := received(), []string{"addTestRedis", "withConnectionStringDirect", "build"}; !slices.Equal(got, want) {
                        t.Errorf("the AppHost received %v, want %v", got, want)
                    }
                }
 
                func TestConcurrentChildErrorsAreAggregated(t *testing.T) {
                    c := newClient("")
                    builder := newBuilderOn(c)
 
                    errs := make([]error, 64)
                    var ready, done sync.WaitGroup
                    ready.Add(1)
                    done.Add(1)
                    go func() {
                        defer done.Done()
                        ready.Wait()
                        for range errs {
                            _ = builder.Err()
                        }
                    }()
                    for i := range errs {
                        errs[i] = fmt.Errorf("error %d", i)
                        done.Add(1)
                        go func() {
                            defer done.Done()
                            ready.Wait()
                            if i%2 == 0 {
                                newHandleWrapperBase(&handle{HandleID: "1", TypeID: "type"}, c).setErr(errs[i])
                            } else {
                                newErroredHandleWrapperBase(errs[i], c)
                            }
                        }()
                    }
                    ready.Done()
                    done.Wait()
 
                    got := builder.Err()
                    for _, err := range errs {
                        if !errors.Is(got, err) {
                            t.Errorf("builder.Err() = %v, want it to include %v", got, err)
                        }
                    }
                    if gotLines := strings.Count(got.Error(), "\n") + 1; gotLines != len(errs) {
                        t.Errorf("builder.Err() contains %d errors, want %d", gotLines, len(errs))
                    }
                }
                """, timeout.Token);
 
            var startInfo = new ProcessStartInfo("go")
            {
                WorkingDirectory = directory.FullName,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                ArgumentList = { "test", "-timeout", "30s", "." }
            };
            EnableGoRaceDetection(startInfo);
            using var process = Process.Start(startInfo)!;
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go runtime test failed:{Environment.NewLine}{await stdout}{await stderr}");
            }
            finally
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    private static void EnableGoRaceDetection(ProcessStartInfo startInfo)
    {
        // Linux CI runners provide GCC; Go's race detector needs cgo and a C compiler.
        // Keep ordinary runtime coverage on Windows, where that toolchain is not guaranteed.
        if (OperatingSystem.IsLinux())
        {
            startInfo.ArgumentList.Insert(1, "-race");
            startInfo.Environment["CGO_ENABLED"] = "1";
        }
    }
 
    [Fact]
    public void GenerateDistributedApplication_NullablePrimitiveArrayElementsUsePointers()
    {
        var nullableNumbers = Assert.IsType<AtsTypeRef>(AtsCapabilityScanner.CreateTypeRef(typeof(double?[])));
        var atsContext = new AtsContext
        {
            Capabilities = [],
            HandleTypes = [],
            EnumTypes = [],
            DtoTypes =
            [
                new AtsDtoTypeInfo
                {
                    TypeId = "Tests/NullableArrayDto",
                    Name = "NullableArrayDto",
                    Properties =
                    [
                        new AtsDtoPropertyInfo
                        {
                            Name = "Values",
                            Type = nullableNumbers
                        }
                    ]
                }
            ]
        };
 
        var generated = _generator.GenerateDistributedApplication(atsContext)["aspire.go"];
 
        Assert.Contains("Values []*float64", generated, StringComparison.Ordinal);
    }
 
    [Fact]
    public void ExportedNullablePrimitiveArraysUsePointerInitializers()
    {
        var nullableNumbers = Assert.IsType<AtsTypeRef>(AtsCapabilityScanner.CreateTypeRef(typeof(double?[])));
        var atsContext = new AtsContext
        {
            Capabilities = [],
            HandleTypes = [],
            EnumTypes = [],
            DtoTypes = [],
            ExportedValues =
            [
                new AtsExportedValueInfo
                {
                    OwningAssemblyName = TestTypesAssemblyName,
                    PathSegments = ["NullableArrays", "Numbers"],
                    Value = JsonNode.Parse("[1,null,2.5]"),
                    Type = nullableNumbers
                }
            ]
        };
 
        var generated = _generator.GenerateDistributedApplication(atsContext)["aspire.go"];
 
        Assert.Contains(
            "[]*float64{func(value float64) *float64 { return &value }(1), nil, func(value float64) *float64 { return &value }(2.5)}",
            generated,
            StringComparison.Ordinal);
    }
 
    [Fact]
    public void GenerateDistributedApplication_WithTestTypes_IncludesExportedValues()
    {
        var atsContext = CreateContextFromTestAssembly();
 
        Assert.Contains(atsContext.ExportedValues, value => string.Join(".", value.PathSegments) == "TestConfigs.Default");
        Assert.Contains(atsContext.ExportedValues, value => string.Join(".", value.PathSegments) == "TestConfigs.Profiles.Development");
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("var TestConfigs = struct {", aspireGo);
        Assert.Matches(@"Default\s+\*TestConfigDto", aspireGo);
        Assert.Contains("Profiles struct {", aspireGo);
        Assert.Matches(@"Development\s+\*TestConfigDto", aspireGo);
        Assert.Matches(@"Profiles struct \{\r?\n\t\tDevelopment \*TestConfigDto\r?\n\t\}\r?\n\tSecure\s+\*TestConfigDto", aspireGo);
    }
 
    [Fact]
    public void GenerateDistributedApplication_WithTestTypes_IncludesCapabilities()
    {
        // Arrange
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        // Assert that capabilities are discovered
        Assert.NotEmpty(capabilities);
 
        // Check for specific capabilities (uses AssemblyName/methodName format)
        Assert.Contains(capabilities, c => c.CapabilityId == $"{TestTypesAssemblyName}/addTestRedis");
        Assert.Contains(capabilities, c => c.CapabilityId == $"{TestTypesAssemblyName}/withPersistence");
        Assert.Contains(capabilities, c => c.CapabilityId == $"{TestTypesAssemblyName}/withOptionalString");
    }
 
    [Fact]
    public void GenerateDistributedApplication_WithTestTypes_DeriveCorrectMethodNames()
    {
        // Arrange
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        // Assert method names are derived correctly
        var addTestRedis = capabilities.First(c => c.CapabilityId == $"{TestTypesAssemblyName}/addTestRedis");
        Assert.Equal("addTestRedis", addTestRedis.MethodName);
 
        var withPersistence = capabilities.First(c => c.CapabilityId == $"{TestTypesAssemblyName}/withPersistence");
        Assert.Equal("withPersistence", withPersistence.MethodName);
    }
 
    [Fact]
    public void GenerateDistributedApplication_WithTestTypes_CapturesParameters()
    {
        // Arrange
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        // Assert parameters are captured
        var addTestRedis = capabilities.First(c => c.CapabilityId == $"{TestTypesAssemblyName}/addTestRedis");
        Assert.Equal(2, addTestRedis.Parameters.Count);
        Assert.Equal("Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder", addTestRedis.TargetTypeId);
        Assert.Contains(addTestRedis.Parameters, p => p.Name == "name" && p.Type?.TypeId == "string");
        Assert.Contains(addTestRedis.Parameters, p => p.Name == "port" && p.IsOptional);
    }
 
    [Fact]
    public void Scanner_ReturnsBuilder_TrueForResourceBuilderReturnTypes()
    {
        // Verify that ReturnsBuilder is correctly set to true for methods
        // that return IResourceBuilder<T>
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        // addTestRedis returns IResourceBuilder<TestRedisResource> - should have ReturnsBuilder = true
        var addTestRedis = capabilities.FirstOrDefault(c => c.CapabilityId == $"{TestTypesAssemblyName}/addTestRedis");
        Assert.NotNull(addTestRedis);
        Assert.True(addTestRedis.ReturnsBuilder,
            "addTestRedis returns IResourceBuilder<T> but ReturnsBuilder is false - fluent chaining won't work");
 
        // withPersistence also returns IResourceBuilder<T>
        var withPersistence = capabilities.FirstOrDefault(c => c.CapabilityId == $"{TestTypesAssemblyName}/withPersistence");
        Assert.NotNull(withPersistence);
        Assert.True(withPersistence.ReturnsBuilder,
            "withPersistence returns IResourceBuilder<T> but ReturnsBuilder is false - fluent chaining won't work");
    }
 
    [Fact]
    public async Task Scanner_AddTestRedis_HasCorrectTypeMetadata()
    {
        // Verify the entire capability object for addTestRedis
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        var addTestRedis = capabilities.FirstOrDefault(c => c.CapabilityId == $"{TestTypesAssemblyName}/addTestRedis");
        Assert.NotNull(addTestRedis);
 
        await Verify(addTestRedis).UseFileName("AddTestRedisCapability");
    }
 
    [Fact]
    public async Task Scanner_WithPersistence_HasCorrectExpandedTargets()
    {
        // Verify the entire capability object for withPersistence
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        var withPersistence = capabilities.FirstOrDefault(c => c.CapabilityId == $"{TestTypesAssemblyName}/withPersistence");
        Assert.NotNull(withPersistence);
 
        await Verify(withPersistence).UseFileName("WithPersistenceCapability");
    }
 
    [Fact]
    public async Task Scanner_WithOptionalString_HasCorrectExpandedTargets()
    {
        // Verify withOptionalString (targets IResource, should expand to TestRedisResource)
        var capabilities = ScanCapabilitiesFromTestAssembly();
 
        var withOptionalString = capabilities.FirstOrDefault(c => c.CapabilityId == $"{TestTypesAssemblyName}/withOptionalString");
        Assert.NotNull(withOptionalString);
 
        await Verify(withOptionalString).UseFileName("WithOptionalStringCapability");
    }
 
    [Fact]
    public async Task Scanner_HostingAssembly_AddContainerCapability()
    {
        // Verify the addContainer capability from the real Aspire.Hosting assembly
        var capabilities = ScanCapabilitiesFromHostingAssembly();
 
        var addContainer = capabilities.FirstOrDefault(c => c.CapabilityId == "Aspire.Hosting/addContainer");
        Assert.NotNull(addContainer);
 
        await Verify(addContainer).UseFileName("HostingAddContainerCapability");
    }
 
    [Fact]
    public void RuntimeType_ContainerResource_IsNotInterface()
    {
        // Verify that ContainerResource.IsInterface returns false using runtime reflection
        var containerResourceType = typeof(ContainerResource);
 
        Assert.NotNull(containerResourceType);
        Assert.False(containerResourceType.IsInterface, "ContainerResource should NOT be an interface");
    }
 
    [Fact]
    public void TwoPassScanning_DeduplicatesCapabilities()
    {
        // Verify that when the same capability appears in multiple assemblies,
        // ScanAssemblies deduplicates by CapabilityId.
        var capabilities = ScanCapabilitiesFromBothAssemblies();
 
        // Each capability ID should appear only once
        var duplicates = capabilities
            .GroupBy(c => c.CapabilityId)
            .Where(g => g.Count() > 1)
            .Select(g => g.Key)
            .ToList();
 
        Assert.Empty(duplicates);
    }
 
    [Fact]
    public void TwoPassScanning_MergesHandleTypesFromAllAssemblies()
    {
        // Verify that ScanAssemblies collects handle types from all assemblies
        var result = CreateContextFromBothAssemblies();
 
        // Should have types from Aspire.Hosting (ContainerResource, etc.)
        var containerResourceType = result.HandleTypes
            .FirstOrDefault(t => t.AtsTypeId.Contains("ContainerResource") && !t.AtsTypeId.Contains("IContainer"));
        Assert.NotNull(containerResourceType);
 
        // Should have types from test assembly (TestRedisResource)
        var testRedisType = result.HandleTypes
            .FirstOrDefault(t => t.AtsTypeId.Contains("TestRedisResource"));
        Assert.NotNull(testRedisType);
 
        // TestRedisResource should have IResourceWithEnvironment in its interfaces
        // (inherited via ContainerResource)
        var hasEnvironmentInterface = testRedisType.ImplementedInterfaces
            .Any(i => i.TypeId.Contains("IResourceWithEnvironment"));
        Assert.True(hasEnvironmentInterface,
            "TestRedisResource should implement IResourceWithEnvironment via ContainerResource");
    }
 
    [Fact]
    public async Task TwoPassScanning_GeneratesWithEnvironmentOnTestRedisBuilder()
    {
        // End-to-end test: verify that AddEnvironment appears on TestRedisResource
        // in the generated Go when using 2-pass scanning.
        var atsContext = CreateContextFromBothAssemblies();
 
        // Generate Go
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        // Verify AddEnvironment appears (method should exist for resources that support it)
        Assert.Contains("WithEnvironment", aspireGo);
 
        // Snapshot for detailed verification
        await Verify(aspireGo, extension: "go")
            .UseFileName("TwoPassScanningGeneratedAspire");
    }
 
    [Fact]
    public void GeneratedCode_UsesPascalCaseMethodNames()
    {
        // Verify that the generated Go code uses PascalCase for exported method names
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        // Go exported methods should use PascalCase
        Assert.Contains("AddContainer", aspireGo);
        Assert.Contains("WithEnvironment", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_HasCreateBuilderFunction()
    {
        // Verify that the generated Go code has a CreateBuilder function
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("func CreateBuilder", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_CreateBuilderDefaultsAppHostFilePathFromEnvironment()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("if appHostFilePath, ok := resolved[\"appHostFilePath\"].(string); !ok || appHostFilePath == \"\"", aspireGo);
        Assert.Contains("os.Getenv(\"ASPIRE_APPHOST_FILEPATH\")", aspireGo);
        Assert.Contains("resolved[\"appHostFilePath\"] = appHostFilePath", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_CreateBuilderOmitsEmptyDashboardApplicationName()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("if dashboardApplicationName, ok := resolved[\"dashboardApplicationName\"].(string); ok && dashboardApplicationName == \"\"", aspireGo);
        Assert.Contains("delete(resolved, \"dashboardApplicationName\")", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_CreateBuilderUsesFunctionalOptions()
    {
        var aspireGo = _generator.GenerateDistributedApplication(CreateContextFromBothAssemblies())["aspire.go"];
 
        Assert.Contains("func CreateBuilder(options ...CreateBuilderOption)", aspireGo);
        Assert.Contains("type CreateBuilderOption interface", aspireGo);
        Assert.Contains("type CreateBuilderProjectDirectory string", aspireGo);
        Assert.Contains("func (value CreateBuilderProjectDirectory) apply(options *CreateBuilderOptions)", aspireGo);
        Assert.Contains("func (option *CreateBuilderOptions) apply(options *CreateBuilderOptions)", aspireGo);
        Assert.DoesNotContain("func CreateBuilder(options ...*CreateBuilderOptions)", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_DtoCallbacksReturnMutatedArguments()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("arg0 := callbackArg[*ResourceUrlAnnotation](args, 0)", aspireGo);
        Assert.Contains("cb(arg0)", aspireGo);
        Assert.Contains("\"p0\": serializeValue(arg0)", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_CallbackShimsBuildTheDeclaredResourceBuilderWrapper()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"].ReplaceLineEndings("\n");
        var shim = ExtractGeneratedMethod(
            aspireGo,
            "func (s *executableResource) PublishAsDockerFile(configure func(obj ContainerResource))");
 
        Assert.Contains(
            "cb(callbackHandleArg(s.client, args, 0, newContainerResourceFromHandle))",
            shim,
            StringComparison.Ordinal);
        Assert.Contains("func callbackHandleArg[T any](", files["base.go"], StringComparison.Ordinal);
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedCallbacks_ReceiveResourceBuilderWrapper()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        using var process = new Process();
        try
        {
            var files = _generator.GenerateDistributedApplication(CreateContextFromBothAssemblies());
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "callback_test.go"), """
                package aspire
 
                import (
                    "bufio"
                    "net"
                    "os"
                    "testing"
                )
 
                // recordedCall is a capability invoked by the callback under test.
                type recordedCall struct {
                    capability string
                    builder    any
                }
 
                // connectFakeAppHost connects a client to an in-memory AppHost. When the SDK
                // invokes publishAsDockerFile, the AppHost calls the registered callback with a
                // resource builder handle that carries callbackHandleType, and completes the
                // capability once the callback returns. Every other capability is recorded and
                // answered with its builder argument.
                func connectFakeAppHost(t *testing.T, callbackHandleType string) (*client, <-chan recordedCall) {
                    t.Helper()
                    clientEnd, serverEnd := net.Pipe()
                    c := newClient("")
                    registerWrappers(c)
                    conn := newConnection(clientEnd, c, c.onConnectionClose)
                    c.mu.Lock()
                    c.conn = conn
                    c.mu.Unlock()
                    conn.start()
                    t.Cleanup(c.disconnect)
 
                    calls := make(chan recordedCall, 8)
                    go func() {
                        defer serverEnd.Close()
                        reader := bufio.NewReader(serverEnd)
                        var publishID, publishBuilder any
                        for {
                            msg, err := readMessage(reader)
                            if err != nil {
                                return
                            }
                            if _, isRequest := msg["method"]; !isRequest {
                                // The callback returned, so the capability that invoked it can complete.
                                writeMessage(serverEnd, map[string]any{"jsonrpc": "2.0", "id": publishID, "result": publishBuilder})
                                continue
                            }
                            params := msg["params"].([]any)
                            capability := params[0].(string)
                            args := params[1].(map[string]any)
                            if capability != "Aspire.Hosting/publishAsDockerFile" {
                                calls <- recordedCall{capability, args["builder"]}
                                writeMessage(serverEnd, map[string]any{"jsonrpc": "2.0", "id": msg["id"], "result": args["builder"]})
                                continue
                            }
                            publishID, publishBuilder = msg["id"], args["builder"]
                            writeMessage(serverEnd, map[string]any{
                                "jsonrpc": "2.0",
                                "id":      1000,
                                "method":  "invokeCallback",
                                "params": []any{
                                    args["configure"],
                                    map[string]any{"p0": map[string]any{"$handle": "callback-resource", "$type": callbackHandleType}},
                                },
                            })
                        }
                    }()
                    return c, calls
                }
 
                func TestCallbackReceivesResourceBuilderWrapper(t *testing.T) {
                    // The AppHost marshals an IResourceBuilder<T> callback argument with its closed
                    // generic CLR type as the $type, and no wrapper is registered under that id.
                    // An argument whose $type is registered arrives already wrapped.
                    handleTypes := map[string]string{
                        "closed generic type": os.Getenv("TEST_RESOURCE_BUILDER_TYPE"),
                        "registered type":     "Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource",
                    }
                    for name, handleType := range handleTypes {
                        t.Run(name, func(t *testing.T) {
                            if handleType == "" {
                                t.Fatal("TEST_RESOURCE_BUILDER_TYPE is not set")
                            }
                            c, calls := connectFakeAppHost(t, handleType)
                            root := newExecutableResourceFromHandle(&handle{
                                HandleID: "root-resource",
                                TypeID:   "Aspire.Hosting/Aspire.Hosting.ApplicationModel.ExecutableResource",
                            }, c)
 
                            received := make(chan ContainerResource, 1)
                            root.PublishAsDockerFile(func(obj ContainerResource) {
                                received <- obj
                                if obj != nil {
                                    obj.WithEntrypoint("/bin/app")
                                }
                            })
                            if err := root.Err(); err != nil {
                                t.Fatalf("PublishAsDockerFile failed: %v", err)
                            }
 
                            // PublishAsDockerFile returns only after the callback has returned.
                            select {
                            case obj := <-received:
                                if obj == nil {
                                    t.Fatal("the callback received a nil ContainerResource")
                                }
                            default:
                                t.Fatal("the callback was not invoked")
                            }
                            select {
                            case call := <-calls:
                                builder, _ := call.builder.(map[string]any)
                                if call.capability != "Aspire.Hosting/withEntrypoint" || builder["$handle"] != "callback-resource" {
                                    t.Fatalf("unexpected follow-up call %s on %v", call.capability, call.builder)
                                }
                            default:
                                t.Fatal("the callback wrapper did not invoke a capability")
                            }
                        })
                    }
                }
                """, timeout.Token);
 
            process.StartInfo = new ProcessStartInfo("go")
            {
                WorkingDirectory = directory.FullName,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                ArgumentList = { "test", "-timeout", "30s", "." },
                // The same id the AppHost derives for an IResourceBuilder<T> callback parameter.
                Environment = { ["TEST_RESOURCE_BUILDER_TYPE"] = AtsTypeMapping.DeriveTypeId(typeof(IResourceBuilder<ContainerResource>)) }
            };
            process.Start();
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go callback test failed:{Environment.NewLine}{await stdout}{await stderr}");
            }
            finally
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [RequiresTools(["go"])]
    public async Task GeneratedRuntime_DecodesResourcesTaggedWithTheDeclaredInterface()
    {
        var directory = Directory.CreateTempSubdirectory();
        using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        using var process = new Process();
        try
        {
            var files = _generator.GenerateDistributedApplication(CreateContextFromBothAssemblies());
            foreach (var (name, content) in files)
            {
                await File.WriteAllTextAsync(Path.Combine(directory.FullName, name), content, timeout.Token);
            }
 
            await File.WriteAllTextAsync(Path.Combine(directory.FullName, "declared_interface_test.go"), """
                package aspire
 
                import (
                    "bufio"
                    "net"
                    "os"
                    "testing"
                )
 
                // connectResourceHost connects a client to an in-memory AppHost that answers the
                // capabilities returning resources. The AppHost tags each handle it returns with the
                // type its capability declares, and Resource has no generated implementation.
                func connectResourceHost(t *testing.T, resourceType string) *client {
                    t.Helper()
                    clientEnd, serverEnd := net.Pipe()
                    c := newClient("")
                    registerWrappers(c)
                    conn := newConnection(clientEnd, c, c.onConnectionClose)
                    c.mu.Lock()
                    c.conn = conn
                    c.mu.Unlock()
                    conn.start()
                    t.Cleanup(c.disconnect)
 
                    resource := func(id string) map[string]any {
                        return map[string]any{"$handle": id, "$type": resourceType}
                    }
                    go func() {
                        defer serverEnd.Close()
                        reader := bufio.NewReader(serverEnd)
                        for {
                            msg, err := readMessage(reader)
                            if err != nil {
                                return
                            }
                            reply := map[string]any{"jsonrpc": "2.0", "id": msg["id"]}
                            params, _ := msg["params"].([]any)
                            switch {
                            case len(params) > 0 && params[0] == "Aspire.Hosting/getResources":
                                reply["result"] = []any{resource("resource-1"), resource("resource-2")}
                            case len(params) > 0 && params[0] == "Aspire.Hosting/findResourceByName":
                                reply["result"] = resource("resource-1")
                            default:
                                reply["error"] = map[string]any{"code": -32601, "message": "unexpected request"}
                            }
                            writeMessage(serverEnd, reply)
                        }
                    }()
                    return c
                }
 
                func newTestModel(t *testing.T) DistributedApplicationModel {
                    t.Helper()
                    resourceType := os.Getenv("TEST_RESOURCE_TYPE")
                    if resourceType == "" {
                        t.Fatal("TEST_RESOURCE_TYPE is not set")
                    }
                    return newDistributedApplicationModelFromHandle(&handle{
                        HandleID: "model",
                        TypeID:   "Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel",
                    }, connectResourceHost(t, resourceType))
                }
 
                func TestListOfResourcesDecodesIntoTheMarkerInterface(t *testing.T) {
                    resources, err := newTestModel(t).GetResources()
                    if err != nil {
                        t.Fatalf("GetResources failed: %v", err)
                    }
                    if len(resources) != 2 {
                        t.Fatalf("GetResources returned %d resources, want 2", len(resources))
                    }
                    for i, want := range []string{"resource-1", "resource-2"} {
                        if resources[i] == nil || resources[i].getHandle().HandleID != want {
                            t.Errorf("resource %d: got %v, want a wrapper for handle %s", i, resources[i], want)
                        }
                    }
                }
 
                func TestSingleResourceDecodesIntoTheMarkerInterface(t *testing.T) {
                    model := newTestModel(t)
                    found := model.FindResourceByName("api")
                    if err := model.Err(); err != nil {
                        t.Fatalf("FindResourceByName failed: %v", err)
                    }
                    if found == nil || found.getHandle().HandleID != "resource-1" {
                        t.Fatalf("FindResourceByName returned %v, want a wrapper for handle resource-1", found)
                    }
                }
                """, timeout.Token);
 
            process.StartInfo = new ProcessStartInfo("go")
            {
                WorkingDirectory = directory.FullName,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                ArgumentList = { "test", "-timeout", "30s", "." },
                // The same id the AppHost derives for the IResource type that GetResources declares.
                Environment = { ["TEST_RESOURCE_TYPE"] = AtsTypeMapping.DeriveTypeId(typeof(IResource)) }
            };
            process.Start();
            var stdout = process.StandardOutput.ReadToEndAsync(timeout.Token);
            var stderr = process.StandardError.ReadToEndAsync(timeout.Token);
            try
            {
                await process.WaitForExitAsync(timeout.Token);
                Assert.True(process.ExitCode == 0, $"Go declared-interface test failed:{Environment.NewLine}{await stdout}{await stderr}");
            }
            finally
            {
                if (!process.HasExited)
                {
                    process.Kill(entireProcessTree: true);
                    await process.WaitForExitAsync();
                }
            }
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public void GeneratedCode_RequiredNullablePrimitiveAndEnumSettersSendNullValues()
    {
        var atsContext = CreateContextFromBothAssemblies();
        var aspireGo = _generator.GenerateDistributedApplication(atsContext)["aspire.go"].ReplaceLineEndings("\n");
        var primitiveSetter = ExtractGeneratedMethod(
            aspireGo,
            "func (s *endpointUpdateContext) SetPort(value *int)");
        var enumSetter = ExtractGeneratedMethod(
            aspireGo,
            "func (s *containerBuildOptionsCallbackContext) SetDestination(value *ContainerImageDestination)");
 
        Assert.Contains("reqArgs[\"value\"] = serializeValue(value)", primitiveSetter, StringComparison.Ordinal);
        Assert.DoesNotContain("if value != nil", primitiveSetter, StringComparison.Ordinal);
        Assert.Contains("reqArgs[\"value\"] = serializeValue(value)", enumSetter, StringComparison.Ordinal);
        Assert.DoesNotContain("if value != nil", enumSetter, StringComparison.Ordinal);
    }
 
    [Theory]
    [InlineData(
        "func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithConfig(config *TestConfigDto)",
        "config",
        "config != nil")]
    [InlineData(
        "func (s *testRedisResource) WithConnectionString(connectionString *ReferenceExpression)",
        "connectionString",
        "connectionString != nil")]
    [InlineData(
        "func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUrl(url any",
        "url",
        "!isNil(url)")]
    public void GeneratedCode_RequiredNonNullableNilableArgumentsOmitNilValues(
        string signature,
        string parameterName,
        string nilGuard)
    {
        var atsContext = CreateContextFromBothAssemblies();
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"].ReplaceLineEndings("\n");
        var method = ExtractGeneratedMethod(aspireGo, signature);
 
        Assert.Matches(
            $@"if {Regex.Escape(nilGuard)} \{{\s+reqArgs\[""{parameterName}""\] = serializeValue\({parameterName}\)\s+\}}",
            method);
        Assert.Contains("func isNil(value any) bool", files["base.go"], StringComparison.Ordinal);
    }
 
    [Fact]
    public void GeneratedCode_InteractionInputValueHelpersHandleNullableValues()
    {
        var atsContext = CreateContextFromBothAssemblies();
        var aspireGo = _generator.GenerateDistributedApplication(atsContext)["aspire.go"].ReplaceLineEndings("\n");
 
        var value = ExtractGeneratedMethod(
            aspireGo,
            "func (s *interactionInputCollection) Value(name string) (string, error)");
        Assert.Contains("input, err := s.Get(name)", value, StringComparison.Ordinal);
        Assert.Contains("if input == nil || input.Value == nil", value, StringComparison.Ordinal);
        Assert.Contains("return *input.Value, nil", value, StringComparison.Ordinal);
 
        var requiredValue = ExtractGeneratedMethod(
            aspireGo,
            "func (s *interactionInputCollection) RequiredValue(name string) (string, error)");
        Assert.Contains("input, err := s.Required(name)", requiredValue, StringComparison.Ordinal);
        Assert.Contains("if input.Value == nil", requiredValue, StringComparison.Ordinal);
        Assert.Contains("return *input.Value, nil", requiredValue, StringComparison.Ordinal);
    }
 
    [Fact]
    public void GeneratedCode_CallbackArgsSkipUndecodableStructFields()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var baseGo = files["base.go"];
 
        Assert.Contains("func decodeStructFields[T any](raw any) (T, bool)", baseGo);
        Assert.Contains("fieldInfo.Tag.Get(\"json\")", baseGo);
    }
 
    [Fact]
    public void GeneratedCode_HasGoModFile()
    {
        // Verify that go.mod file is generated
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
 
        Assert.Contains("go.mod", files.Keys);
        Assert.Contains("module apphost/modules/aspire", files["go.mod"]);
    }
 
    [Fact]
    public void GenerateDistributedApplication_HostingAssembly_SanitizesGoKeywordParameters()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Matches(@"func \(s \*[^\)]*\) WithRelationship\([^)]*type_ string\)", aspireGo);
        Assert.DoesNotMatch(@"func \(s \*[^\)]*\) WithRelationship\([^)]*\btype string\)", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_UsesFunctionalOptionsForOptionalParameters()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("WithHttpEndpoint(options ...WithHttpEndpointOption)", aspireGo);
        Assert.Contains("type WithHttpEndpointOption interface", aspireGo);
        Assert.Contains("type HttpEndpointPort int", aspireGo);
        Assert.Contains("func (value HttpEndpointPort) apply(options *WithHttpEndpointOptions)", aspireGo);
        Assert.Contains("type HttpEndpointIsProxied bool", aspireGo);
        Assert.Contains("func (value HttpEndpointIsProxied) apply(options *WithHttpEndpointOptions)", aspireGo);
        Assert.DoesNotContain("withHttpEndpointOptionFunc", aspireGo);
        Assert.Contains("opt.apply(merged)", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_WrapsOptionalDtoAndCancellationTokenInFunctionalOptions()
    {
        var atsContext = CreateContextFromBothAssemblies();
 
        var files = _generator.GenerateDistributedApplication(atsContext);
        var aspireGo = files["aspire.go"];
 
        Assert.Contains("PromptInput(title string, message string, input InteractionInputBuilder, options ...PromptInputOption)", aspireGo);
        Assert.Contains("type PromptInputOptions struct", aspireGo);
        Assert.Matches(@"Options\s+\*InteractionInputsDialogOptions `json:""options,omitempty""`", aspireGo);
        Assert.Contains("func PromptInputOptionsValue(value *InteractionInputsDialogOptions) PromptInputOption", aspireGo);
        Assert.Contains("func PromptInputCancellationToken(value *CancellationToken) PromptInputOption", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_ReservesDistinctExportedOptionNames()
    {
        var stringType = new AtsTypeRef
        {
            TypeId = AtsConstants.String,
            ClrType = typeof(string),
            Category = AtsTypeCategory.Primitive
        };
        var int64Type = new AtsTypeRef
        {
            TypeId = AtsConstants.Number,
            ClrType = typeof(long),
            Category = AtsTypeCategory.Primitive
        };
        var voidType = new AtsTypeRef { TypeId = AtsConstants.Void, Category = AtsTypeCategory.Primitive };
        var capabilities = new[]
        {
            new AtsCapabilityInfo
            {
                CapabilityId = "Tests/withFoo",
                MethodName = "withFoo",
                Parameters = [new AtsParameterInfo { Name = "barBaz", Type = stringType, IsOptional = true }],
                ReturnType = voidType
            },
            new AtsCapabilityInfo
            {
                CapabilityId = "Tests/withFooBar",
                MethodName = "withFooBar",
                Parameters = [new AtsParameterInfo { Name = "baz", Type = stringType, IsOptional = true }],
                ReturnType = voidType
            },
            new AtsCapabilityInfo
            {
                CapabilityId = "Tests/withInt64",
                MethodName = "withInt64",
                Parameters = [new AtsParameterInfo { Name = "ptr", Type = int64Type, IsOptional = true }],
                ReturnType = voidType
            }
        };
        var context = new AtsContext
        {
            Capabilities = capabilities,
            HandleTypes = [],
            DtoTypes =
            [
                new AtsDtoTypeInfo
                {
                    TypeId = "Tests/FooBarBaz",
                    Name = "FooBarBaz",
                    Properties = []
                }
            ],
            EnumTypes = []
        };
 
        var aspireGo = new AtsGoCodeGenerator().GenerateDistributedApplication(context)["aspire.go"];
        var optionTypes = Regex.Matches(aspireGo, @"(?m)^type (FooBarBaz\d+) string$")
            .Select(match => match.Groups[1].Value)
            .ToList();
 
        Assert.Equal(2, optionTypes.Count);
        Assert.Equal(2, optionTypes.Distinct(StringComparer.Ordinal).Count());
        Assert.All(optionTypes, name => Assert.True(char.IsUpper(name[0])));
        Assert.DoesNotContain("type Int64Ptr int64", aspireGo);
        Assert.Contains("type Int64Ptr2 int64", aspireGo);
    }
 
    [Fact]
    public void GeneratedCode_MapsClrNumericTypesAndOptionality()
    {
        var numericTypes = new (string Name, Type ClrType, string GoType)[]
        {
            ("Integer", typeof(int), "int"),
            ("Long", typeof(long), "int64"),
            ("Double", typeof(double), "float64"),
            ("Float", typeof(float), "float32"),
            ("Short", typeof(short), "int16"),
            ("Byte", typeof(byte), "uint8"),
            ("Decimal", typeof(decimal), "float64"),
            ("UShort", typeof(ushort), "uint16"),
            ("UInt", typeof(uint), "uint"),
            ("ULong", typeof(ulong), "uint64"),
            ("SByte", typeof(sbyte), "int8")
        };
        var atsContext = new AtsContext
        {
            Capabilities = [],
            HandleTypes = [],
            EnumTypes = [],
            DtoTypes =
            [
                new AtsDtoTypeInfo
                {
                    TypeId = "Tests/NumericDto",
                    Name = "NumericDto",
                    Properties = numericTypes.Select(type => new AtsDtoPropertyInfo
                    {
                        Name = type.Name,
                        Type = new AtsTypeRef
                        {
                            TypeId = AtsConstants.Number,
                            ClrType = type.ClrType,
                            Category = AtsTypeCategory.Primitive
                        },
                        IsOptional = type.Name == "Integer"
                    }).ToList()
                }
            ]
        };
 
        var aspireGo = _generator.GenerateDistributedApplication(atsContext)["aspire.go"];
 
        foreach (var (name, _, goType) in numericTypes)
        {
            var expectedType = name == "Integer" ? $"*{goType}" : goType;
            Assert.Matches($@"{name}\s+{Regex.Escape(expectedType)}\b", aspireGo);
        }
    }
 
    [Fact]
    public void GeneratedCode_FiltersObsoleteSymbolsButKeepsRequiredWireTypes()
    {
        var context = CreateContextFromTestAssembly();
        var obsoleteContext = Assert.Single(context.HandleTypes, type => type.ClrType == typeof(GoObsoleteContext));
        var obsoleteDto = Assert.Single(context.DtoTypes, type => type.ClrType == typeof(GoObsoleteDto));
        var obsoleteEnum = Assert.Single(context.EnumTypes, type => type.ClrType == typeof(GoObsoleteEnum));
 
        Assert.False(obsoleteContext.IsObsolete);
        Assert.True(Assert.Single(obsoleteDto.Properties, property => property.Name == "Legacy").IsObsolete);
        Assert.True(Assert.Single(obsoleteEnum.ValueInfos, value => value.Name == "Legacy").IsObsolete);
        Assert.Contains(context.Capabilities, capability => capability.MethodName == "getLegacyValue" && capability.IsObsolete);
        Assert.Contains(context.ExportedValues, value => value.PathSegments[^1] == "LegacyValue" && value.IsObsolete);
 
        var aspireGo = new AtsGoCodeGenerator().GenerateDistributedApplication(context)["aspire.go"];
 
        Assert.Contains("type GoObsoleteDto struct", aspireGo);
        Assert.Contains("Current string", aspireGo);
        Assert.DoesNotContain("Legacy string", aspireGo);
        Assert.Contains("type GoObsoleteEnum string", aspireGo);
        Assert.Contains("GoObsoleteEnumCurrent", aspireGo);
        Assert.DoesNotContain("GoObsoleteEnumLegacy", aspireGo);
        Assert.DoesNotContain("GetLegacyValue", aspireGo);
        Assert.DoesNotMatch(@"type GoObsoleteContext interface \{[^}]*\bLegacy\(\)", aspireGo);
        Assert.DoesNotContain("LegacyValue", aspireGo);
        Assert.DoesNotContain("type GoUnusedObsoleteDto struct", aspireGo);
 
        // These obsolete types remain because active methods still use them on the wire.
        Assert.Contains("type GoObsoleteWireDto struct", aspireGo);
        Assert.Contains("type GoObsoleteWireHandle interface", aspireGo);
        Assert.Contains("GetObsoleteWireDto() (*GoObsoleteWireDto, error)", aspireGo);
        Assert.Contains("GetObsoleteWireHandle() GoObsoleteWireHandle", aspireGo);
 
        var api = new AtsGoCodeGenerator().BuildApiModel(
            context,
            TestTypesAssemblyName,
            "1.0.0",
            [TestTypesAssemblyName],
            CancellationToken.None);
        var apiItems = api.Modules.Single().Items;
        Assert.DoesNotContain(
            apiItems.SelectMany(item => item.Members),
            member => member.Name is "Legacy" or "GetLegacyValue" || member.Name.EndsWith("LegacyValue", StringComparison.Ordinal));
        Assert.DoesNotContain(apiItems, item => item.Name == "GoUnusedObsoleteDto");
        Assert.Contains(apiItems, item => item.Name == "GoObsoleteWireDto");
        Assert.Contains(apiItems, item => item.Name == "GoObsoleteWireHandle");
    }
 
    private static List<AtsCapabilityInfo> ScanCapabilitiesFromTestAssembly()
    {
        var testAssembly = LoadTestAssembly();
 
        // Scan capabilities from the test assembly
        var result = AtsCapabilityScanner.ScanAssembly(testAssembly);
        return result.Capabilities;
    }
 
    private static AtsContext CreateContextFromTestAssembly()
    {
        var testAssembly = LoadTestAssembly();
 
        // Scan capabilities from the test assembly
        var result = AtsCapabilityScanner.ScanAssembly(testAssembly);
        return result.ToAtsContext();
    }
 
    private static Assembly LoadTestAssembly()
    {
        // Get the test assembly at runtime (TypeScript tests assembly has the TestTypes)
        return typeof(TestRedisResource).Assembly;
    }
 
    private static List<AtsCapabilityInfo> ScanCapabilitiesFromHostingAssembly()
    {
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
        return result.Capabilities;
    }
 
    private static List<AtsCapabilityInfo> ScanCapabilitiesFromBothAssemblies()
    {
        var (testAssembly, hostingAssembly) = LoadBothAssemblies();
 
        // Use ScanAssemblies for proper cross-assembly expansion
        var result = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, testAssembly]);
        return result.Capabilities;
    }
 
    private static AtsContext CreateContextFromBothAssemblies()
    {
        var (testAssembly, hostingAssembly) = LoadBothAssemblies();
 
        // Use ScanAssemblies for proper cross-assembly expansion and enum collection
        var result = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, testAssembly]);
        return result.ToAtsContext();
    }
 
    private static (Assembly testAssembly, Assembly hostingAssembly) LoadBothAssemblies()
    {
        var testAssembly = typeof(TestRedisResource).Assembly;
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        return (testAssembly, hostingAssembly);
    }
 
    private static string ExtractGeneratedMethod(string generatedCode, string signature)
    {
        var methodStart = generatedCode.IndexOf(signature, StringComparison.Ordinal);
        Assert.True(methodStart >= 0, $"Generated method not found: {signature}");
        var methodEnd = generatedCode.IndexOf("\n}\n", methodStart, StringComparison.Ordinal);
        Assert.True(methodEnd >= 0, $"Generated method is incomplete: {signature}");
 
        return generatedCode[methodStart..methodEnd];
    }
 
}