// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <span>

#include "dawn/wire/Wire.h"
#include "dawn/wire/WireClient.h"
#include "dawn/wire/WireCmd_autogen.h"
#include "dawn/wire/WireServer.h"
#include "src/dawn/common/StringViewUtils.h"
#include "src/dawn/tests/MockCallback.h"
#include "src/dawn/tests/StringViewMatchers.h"
#include "src/dawn/tests/unittests/wire/WireTest.h"
#include "src/dawn/utils/TerribleCommandBuffer.h"
#include "src/dawn/wire/BufferConsumer.h"
#include "src/dawn/wire/ChunkedCommandSerializer.h"
#include "src/dawn/wire/client/Client.h"
#include "src/dawn/wire/server/Server.h"

namespace dawn::wire {
namespace {

using testing::_;
using testing::EmptySizedString;
using testing::InvokeWithoutArgs;
using testing::IsNull;
using testing::MockCppCallback;
using testing::NonEmptySizedString;
using testing::NotNull;
using testing::Return;
using testing::WithArg;

// Fixture that helps execute specific commands through the wire that may not be possible to trigger
// through usage of the dawn::wire::client. It is even more change detecting than regular dawn::wire
// tests so we should use it only when there are no alternatives.
class WireSpecificCommandTests : public WireTest {
  protected:
    template <typename Cmd>
    void AddSpecificServerCmd(const Cmd& cmd) {
        CommandSerializer* c2s = GetC2SSerializer();
        ChunkedCommandSerializer serializer(c2s);

        serializer.SerializeCommand(cmd, *GetWireClient()->GetImplForTesting());
    }

    // Intercept a command that will be sent from the client to the server. This involves first
    // capturing the command, deserializing it as if we were the server, and then re-injecting it
    // back into the client command buffer. Note that this does not currently handle object
    // serialization correctly, so when modifying commands with descriptors that include other
    // WebGPU objects, users may need to manually update those members. This is currently a
    // limitation that is hard to address because of the way the commands are [de]serialized in the
    // wire.
    template <typename Cmd, typename ToIntercept, typename Modifier>
    void InterceptServerCmd(ToIntercept toIntercept, Modifier modifier) {
        Cmd cmd;
        utils::TerribleCommandBuffer* c2sBuf = GetC2SCommandBuffer();
        size_t startOffset = c2sBuf->GetOffsetForTesting();
        toIntercept();
        size_t endOffset = c2sBuf->GetOffsetForTesting();
        std::span<const char> subrange = c2sBuf->GetContentSubrange(startOffset, endOffset);
        dawn::wire::DeserializeBuffer deserializeBuffer(subrange.data(), subrange.size());
        EXPECT_EQ(WireResult::Success, cmd.Deserialize(&deserializeBuffer, &mAllocator,
                                                       *GetWireServer()->GetImplForTesting()));

        modifier(&cmd);
        c2sBuf->SetOffsetForTesting(startOffset);
        AddSpecificServerCmd(cmd);
    }

  private:
    WireDeserializeAllocator mAllocator;
};

// Regression test for https://issues.chromium.org/492139412 where a server receiving
// Device::Destroy wouldn't realize that the buffers got unmapped and would try to write into them.
// While it's not exactly possible to replicate the issue with WireTests since there is no
// dawn::native backend that will unmap buffers on destroy, we can check that the ordering of
// commands in the server is such that it will check that the buffer is mapped before writing into
// it.
TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MappedAtCreation) {
    // Create a mapped buffer.
    wgpu::BufferDescriptor descriptor = {};
    descriptor.size = 4;
    descriptor.usage = wgpu::BufferUsage::CopySrc;
    descriptor.mappedAtCreation = true;
    wgpu::Buffer buffer = device.CreateBuffer(&descriptor);

    WGPUBuffer apiBuffer = api.GetNewBuffer();
    EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _))
        .WillOnce(Return(apiBuffer))
        .RetiresOnSaturation();
    FlushClient();

    // Force a device destroy without giving the wire::client a chance to unmap client-side buffers.
    DeviceDestroyCmd cmd;
    cmd.self = device.Get();
    AddSpecificServerCmd(cmd);

    EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1);
    FlushClient();

    // A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd
    // see a crash here since it would write to nullptr).
    EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 0, 4)).WillOnce(Return(nullptr));
    EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1);
    buffer.Unmap();
    FlushClient();
}

// The same test at an offset, to check that it doesn't allow bypassing the null check. It was a
// bug found during review of the fix.
TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MapWriteOffsetNonZero) {
    // Create a mapped buffer.
    wgpu::BufferDescriptor descriptor = {};
    descriptor.size = 8;
    descriptor.usage = wgpu::BufferUsage::MapWrite;
    wgpu::Buffer buffer = device.CreateBuffer(&descriptor);

    WGPUBuffer apiBuffer = api.GetNewBuffer();
    EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _))
        .WillOnce(Return(apiBuffer))
        .RetiresOnSaturation();
    FlushClient();

    // Map the buffer
    buffer.MapAsync(wgpu::MapMode::Write, 4, 4, wgpu::CallbackMode::AllowProcessEvents,
                    [](wgpu::MapAsyncStatus status, wgpu::StringView) {});
    EXPECT_CALL(api, OnBufferMapAsync(apiBuffer, WGPUMapMode_Write, 4, 4, _))
        .WillOnce(InvokeWithoutArgs([&] {
            api.CallBufferMapAsyncCallback(apiBuffer, WGPUMapAsyncStatus_Success,
                                           kEmptyOutputStringView);
        }));

    FlushClient();
    FlushServer();
    instance.ProcessEvents();

    // Force a device destroy without giving the wire::client a chance to unmap client-side buffers.
    DeviceDestroyCmd cmd;
    cmd.self = device.Get();
    AddSpecificServerCmd(cmd);

    EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1);
    FlushClient();

    // A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd
    // see a crash here since it would write to nullptr).
    EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 4, 4)).WillOnce(Return(nullptr));
    EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1);
    buffer.Unmap();
    FlushClient();
}

// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed Device.
TEST_F(WireSpecificCommandTests, RequestDeviceIdReuseAfterInjectedUnregister) {
    // Set up all the mock callbacks and objects.
    MockCppCallback<wgpu::DeviceLostCallback<void>*> deviceLostCb;
    MockCppCallback<wgpu::RequestDeviceCallback<void>*> requestDeviceCb;
    WGPUDevice apiDeviceA = api.GetNewDevice();
    WGPUFuture futureA;
    WGPUDevice apiDeviceB = api.GetNewDevice();
    WGPUFuture futureB;
    auto SetDeviceCallbacks = [](WGPUDevice apiDevice, const WGPUDeviceDescriptor* desc) {
        ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>(apiDevice);
        object->mDeviceLostCallback = desc->deviceLostCallbackInfo.callback;
        object->mDeviceLostUserdata1 = desc->deviceLostCallbackInfo.userdata1;
        object->mDeviceLostUserdata2 = desc->deviceLostCallbackInfo.userdata2;
    };

    // Add the first request for a device and capture the C->S command for it.
    wgpu::DeviceDescriptor deviceDescA = {};
    deviceDescA.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
                                      deviceLostCb.Callback());
    AdapterRequestDeviceCmd requestA = {};
    InterceptServerCmd<AdapterRequestDeviceCmd>(
        [&]() {
            adapter.RequestDevice(&deviceDescA, wgpu::CallbackMode::AllowSpontaneous,
                                  requestDeviceCb.Callback());
        },
        [&](AdapterRequestDeviceCmd* cmd) { requestA = *cmd; });

    // Immediately unregister the device that was reserved for the first request. This is not
    // normally possible, so we inject the command directly.
    UnregisterObjectCmd unregisterA = {};
    unregisterA.objectType = ObjectType::Device;
    unregisterA.objectId = requestA.deviceObjectHandle.id;
    AddSpecificServerCmd(unregisterA);

    // Add a second request for a device that attempts to reuse the same id that was originally
    // reserved for the first request.
    wgpu::DeviceDescriptor deviceDescB = {};
    deviceDescB.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
                                      deviceLostCb.Callback());
    AdapterRequestDeviceCmd requestB = {};
    InterceptServerCmd<AdapterRequestDeviceCmd>(
        [&]() {
            adapter.RequestDevice(&deviceDescB, wgpu::CallbackMode::AllowSpontaneous,
                                  requestDeviceCb.Callback());
        },
        [&](AdapterRequestDeviceCmd* cmd) {
            cmd->deviceObjectHandle.id = requestA.deviceObjectHandle.id;
            cmd->deviceObjectHandle.generation = requestA.deviceObjectHandle.generation + 1;
            requestB = *cmd;
        });

    EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _))
        .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
            SetDeviceCallbacks(apiDeviceA, desc);
            futureA = api.GetLastFuture();
        }))
        .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
            SetDeviceCallbacks(apiDeviceB, desc);
            futureB = api.GetLastFuture();
        }));
    FlushClient();

    // Emulate the backend server completing the first request for a device successfully. Even
    // though the backend successfully created the device, because the client has already asked to
    // unregister that reserved slot, the server should just immediately release the successfully
    // created device.
    EXPECT_CALL(api, DeviceRelease(apiDeviceA)).Times(1);
    api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceA,
                                         kEmptyOutputStringView, futureA);
    // Additionally verify that the server does not back the device.
    EXPECT_EQ(GetWireServer()->GetDevice(requestA.deviceObjectHandle.id,
                                         requestA.deviceObjectHandle.generation),
              nullptr);

    // When we flush the server, the RequestDevice callback for the first request should be called
    // with callback cancelled, and the DeviceLost callback should also fire once for the first
    // requested device.
    EXPECT_CALL(requestDeviceCb, Call(wgpu::RequestDeviceStatus::CallbackCancelled, IsNull(),
                                      NonEmptySizedString()));
    EXPECT_CALL(deviceLostCb, Call).Times(1);
    FlushServer();

    // Emulate the backend server completing the second request for a device successfully.
    api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceB,
                                         kEmptyOutputStringView, futureB);
    // Verify that the server backs the device.
    EXPECT_EQ(GetWireServer()->GetDevice(requestB.deviceObjectHandle.id,
                                         requestB.deviceObjectHandle.generation),
              apiDeviceB);

    // When the server completes the second request for a device, the RequestDevice callback for
    // the second request should be called with success, and the DeviceLost callback should also
    // fire once for the second requested device because we don't save the device from the
    // RequestDevice callback.
    EXPECT_CALL(requestDeviceCb,
                Call(wgpu::RequestDeviceStatus::Success, NotNull(), EmptySizedString()));
    EXPECT_CALL(deviceLostCb, Call).Times(1);
    FlushServer();

    // Finally, since we dropped the device on the client side, it should be Unregistered from the
    // client, and correspondingly released on the server side once the commands are flushed. Note
    // that the client flush should still fail though because we injected a different ID for the
    // device than the client frontend actually allocated for the second device. This means that the
    // wire client frontend will try to Unregister a non-existent object as a result of the modified
    // and injected second request which will cause the server to fail.
    EXPECT_CALL(api, DeviceRelease(apiDeviceB)).Times(1);
    FlushClient(false);
}

// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed Adapter.
TEST_F(WireSpecificCommandTests, RequestAdapterIdReuseAfterInjectedUnregister) {
    // Set up all the mock callbacks and objects.
    MockCppCallback<wgpu::RequestAdapterCallback<void>*> requestAdapterCb;
    WGPUAdapter apiAdapterA = api.GetNewAdapter();
    WGPUFuture futureA;
    WGPUAdapter apiAdapterB = api.GetNewAdapter();
    WGPUFuture futureB;

    // Add the first request for an adapter.
    InstanceRequestAdapterCmd requestA = {};
    InterceptServerCmd<InstanceRequestAdapterCmd>(
        [&]() {
            instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
                                    requestAdapterCb.Callback());
        },
        [&](InstanceRequestAdapterCmd* cmd) { requestA = *cmd; });

    // Immediately unregister the adapter that was reserved for the first request. This is not
    // normally possible, so we inject the command directly.
    UnregisterObjectCmd unregisterA = {};
    unregisterA.objectType = ObjectType::Adapter;
    unregisterA.objectId = requestA.adapterObjectHandle.id;
    AddSpecificServerCmd(unregisterA);

    // Add a second request for an adapter that attempts to reuse the same id that was originally
    // reserved for the first request.
    InstanceRequestAdapterCmd requestB = {};
    InterceptServerCmd<InstanceRequestAdapterCmd>(
        [&]() {
            instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
                                    requestAdapterCb.Callback());
        },
        [&](InstanceRequestAdapterCmd* cmd) {
            cmd->adapterObjectHandle.id = requestA.adapterObjectHandle.id;
            cmd->adapterObjectHandle.generation = requestA.adapterObjectHandle.generation + 1;
            requestB = *cmd;
        });

    EXPECT_CALL(api, OnInstanceRequestAdapter(apiInstance, IsNull(), _))
        .WillOnce([&]() { futureA = api.GetLastFuture(); })
        .WillOnce([&]() { futureB = api.GetLastFuture(); });
    FlushClient();

    // Emulate the backend server completing the first request for an adapter successfully. Even
    // though the backend successfully created the adapter, because the client has already asked to
    // unregister that reserved slot, the server should just immediately release the successfully
    // created adapter.
    EXPECT_CALL(api, AdapterRelease(apiAdapterA)).Times(1);
    api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
                                           apiAdapterA, kEmptyOutputStringView, futureA);

    // When we flush the server, the RequestAdapter callback for the first request should be called
    // with callback cancelled.
    EXPECT_CALL(requestAdapterCb, Call(wgpu::RequestAdapterStatus::CallbackCancelled, IsNull(),
                                       NonEmptySizedString()));
    FlushServer();

    // Emulate the backend server completing the second request for an adapter successfully.
    api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
                                           apiAdapterB, kEmptyOutputStringView, futureB);

    // When the server completes the second request for an adapter, the RequestAdapter callback for
    // the second request should be called with success.
    EXPECT_CALL(requestAdapterCb,
                Call(wgpu::RequestAdapterStatus::Success, NotNull(), EmptySizedString()));
    FlushServer();

    // Finally, since we dropped the adapter on the client side, it should be Unregistered from the
    // client, and correspondingly released on the server side once the commands are flushed. Note
    // that the client flush should still fail though because we injected a different ID for the
    // adapter than the client frontend actually allocated for the second adapter. This means that
    // the wire client frontend will try to Unregister a non-existent object as a result of the
    // modified and injected second request which will cause the server to fail.
    EXPECT_CALL(api, AdapterRelease(apiAdapterB)).Times(1);
    FlushClient(false);
}

// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed ComputePipeline. See
// the RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for
// each step throughout the test.
TEST_F(WireSpecificCommandTests, CreateComputePipelineAsyncIdReuseAfterInjectedUnregister) {
    wgpu::ShaderModuleDescriptor shaderDesc = {};
    wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc);
    WGPUShaderModule apiShader = api.GetNewShaderModule();
    EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader));
    FlushClient();

    wgpu::ComputePipelineDescriptor descriptor = {};
    descriptor.compute.module = shader;

    MockCppCallback<wgpu::CreateComputePipelineAsyncCallback<void>*> createComputePipelineCb;
    WGPUComputePipeline apiPipelineA = api.GetNewComputePipeline();
    WGPUFuture futureA;
    WGPUComputePipeline apiPipelineB = api.GetNewComputePipeline();
    WGPUFuture futureB;

    DeviceCreateComputePipelineAsyncCmd requestA = {};
    InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>(
        [&]() {
            device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
                                              createComputePipelineCb.Callback());
        },
        [&](DeviceCreateComputePipelineAsyncCmd* cmd) {
            requestA = *cmd;
            // Manually fix the shader module since the [de]serialization can't handle objects.
            const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module =
                shader.Get();
        });

    UnregisterObjectCmd unregisterA = {};
    unregisterA.objectType = ObjectType::ComputePipeline;
    unregisterA.objectId = requestA.pipelineObjectHandle.id;
    AddSpecificServerCmd(unregisterA);

    DeviceCreateComputePipelineAsyncCmd requestB = {};
    InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>(
        [&]() {
            device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
                                              createComputePipelineCb.Callback());
        },
        [&](DeviceCreateComputePipelineAsyncCmd* cmd) {
            cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id;
            cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1;
            requestB = *cmd;
            // Manually fix the shader module since the [de]serialization can't handle objects.
            const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module =
                shader.Get();
        });

    EXPECT_CALL(api, OnDeviceCreateComputePipelineAsync(apiDevice, NotNull(), _))
        .WillOnce([&]() { futureA = api.GetLastFuture(); })
        .WillOnce([&]() { futureB = api.GetLastFuture(); });
    FlushClient();

    EXPECT_CALL(api, ComputePipelineRelease(apiPipelineA)).Times(1);
    api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice,
                                                     WGPUCreatePipelineAsyncStatus_Success,
                                                     apiPipelineA, kEmptyOutputStringView, futureA);

    EXPECT_CALL(createComputePipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled,
                                              IsNull(), NonEmptySizedString()));
    FlushServer();

    api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice,
                                                     WGPUCreatePipelineAsyncStatus_Success,
                                                     apiPipelineB, kEmptyOutputStringView, futureB);

    EXPECT_CALL(createComputePipelineCb,
                Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString()));
    FlushServer();

    EXPECT_CALL(api, ComputePipelineRelease(apiPipelineB)).Times(1);
    FlushClient(false);
}

// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed RenderPipeline. See the
// RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for
// each step throughout the test.
TEST_F(WireSpecificCommandTests, CreateRenderPipelineAsyncIdReuseAfterInjectedUnregister) {
    wgpu::ShaderModuleDescriptor shaderDesc = {};
    wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc);
    WGPUShaderModule apiShader = api.GetNewShaderModule();
    EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader));
    FlushClient();

    wgpu::FragmentState fragment = {};
    fragment.module = shader;
    wgpu::RenderPipelineDescriptor descriptor = {};
    descriptor.vertex.module = shader;
    descriptor.fragment = &fragment;

    MockCppCallback<wgpu::CreateRenderPipelineAsyncCallback<void>*> createRenderPipelineCb;
    WGPURenderPipeline apiPipelineA = api.GetNewRenderPipeline();
    WGPUFuture futureA;
    WGPURenderPipeline apiPipelineB = api.GetNewRenderPipeline();
    WGPUFuture futureB;

    DeviceCreateRenderPipelineAsyncCmd requestA = {};
    InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>(
        [&]() {
            device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
                                             createRenderPipelineCb.Callback());
        },
        [&](DeviceCreateRenderPipelineAsyncCmd* cmd) {
            requestA = *cmd;
            // Manually fix the shader module since the [de]serialization can't handle objects.
            auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor);
            desc->vertex.module = shader.Get();
            const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get();
        });

    UnregisterObjectCmd unregisterA = {};
    unregisterA.objectType = ObjectType::RenderPipeline;
    unregisterA.objectId = requestA.pipelineObjectHandle.id;
    AddSpecificServerCmd(unregisterA);

    DeviceCreateRenderPipelineAsyncCmd requestB = {};
    InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>(
        [&]() {
            device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
                                             createRenderPipelineCb.Callback());
        },
        [&](DeviceCreateRenderPipelineAsyncCmd* cmd) {
            cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id;
            cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1;
            requestB = *cmd;
            // Manually fix the shader module since the [de]serialization can't handle objects.
            auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor);
            desc->vertex.module = shader.Get();
            const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get();
        });

    EXPECT_CALL(api, OnDeviceCreateRenderPipelineAsync(apiDevice, NotNull(), _))
        .WillOnce([&]() { futureA = api.GetLastFuture(); })
        .WillOnce([&]() { futureB = api.GetLastFuture(); });
    FlushClient();

    EXPECT_CALL(api, RenderPipelineRelease(apiPipelineA)).Times(1);
    api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice,
                                                    WGPUCreatePipelineAsyncStatus_Success,
                                                    apiPipelineA, kEmptyOutputStringView, futureA);

    EXPECT_CALL(createRenderPipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled,
                                             IsNull(), NonEmptySizedString()));
    FlushServer();

    api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice,
                                                    WGPUCreatePipelineAsyncStatus_Success,
                                                    apiPipelineB, kEmptyOutputStringView, futureB);

    EXPECT_CALL(createRenderPipelineCb,
                Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString()));
    FlushServer();

    EXPECT_CALL(api, RenderPipelineRelease(apiPipelineB)).Times(1);
    FlushClient(false);
}

}  // anonymous namespace
}  // namespace dawn::wire
