text: // Copyright 2019 The Fuchsia Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
  
  #include "src/developer/debug/debug_agent/debug_agent.h"
  
  #include <zircon/status.h>
  
  #include <gtest/gtest.h>
  
  #include "src/developer/debug/debug_agent/arch_provider_impl.h"
  #include "src/developer/debug/debug_agent/local_stream_backend.h"
  #include "src/developer/debug/debug_agent/mock_object_provider.h"
  #include "src/developer/debug/debug_agent/mock_process.h"
  #include "src/developer/debug/debug_agent/system_info.h"
  #include "src/developer/debug/debug_agent/test_utils.h"
  #include "src/developer/debug/ipc/agent_protocol.h"
  #include "src/developer/debug/ipc/message_writer.h"
  #include "src/developer/debug/shared/platform_message_loop.h"
  #include "src/lib/fxl/logging.h"
  #include "src/lib/fxl/strings/string_printf.h"
  
  using namespace fuchsia::exception;
  
  namespace debug_agent {
  namespace {
  
  // Setup -------------------------------------------------------------------------------------------
  
  class DebugAgentMessageLoop : public debug_ipc::PlatformMessageLoop {
   public:
    DebugAgentMessageLoop() {
      std::string error_message;
      bool success = Init(&error_message);
      FXL_CHECK(success) << error_message;
    }
    ~DebugAgentMessageLoop() { Cleanup(); }
  
    void StopWatching(int id) override {}
  
    zx_status_t WatchProcessExceptions(WatchProcessConfig config, WatchHandle* out) override {
      watches_.push_back(std::move(config));
      *out = WatchHandle(this, next_watch_id_++);
      return ZX_OK;
    }
  
    const std::vector<WatchProcessConfig> watches() const { return watches_; }
  
   private:
    int next_watch_id_ = 1;
  
    std::vector<WatchProcessConfig> watches_;
  };
  
  class DebugAgentStreamBackend : public LocalStreamBackend {
   public:
    void HandleAttach(debug_ipc::AttachReply attach_reply) override {
      attach_replies_.push_back(std::move(attach_reply));
    }
  
    void HandleNotifyProcessStarting(debug_ipc::NotifyProcessStarting notification) override {
      process_starts_.push_back(std::move(notification));
    }
  
    void HandleNotifyModules(debug_ipc::NotifyModules modules) override {
      modules_.push_back(std::move(modules));
    }
  
    const std::vector<debug_ipc::AttachReply>& attach_replies() const { return attach_replies_; }
    const std::vector<debug_ipc::NotifyProcessStarting>& process_starts() const {
      return process_starts_;
    }
    const std::vector<debug_ipc::NotifyModules> modules() const { return modules_; }
  
   private:
    std::vector<debug_ipc::AttachReply> attach_replies_;
    std::vector<debug_ipc::NotifyProcessStarting> process_starts_;
    std::vector<debug_ipc::NotifyModules> modules_;
  };
  
  class DebugAgentMockProcess : public MockProcess {
   public:
    DebugAgentMockProcess(DebugAgent* debug_agent, zx_koid_t koid, std::string name,
                          std::shared_ptr<ObjectProvider> object_provider,
                          std::shared_ptr<arch::ArchProvider> arch_provider)
        : MockProcess(debug_agent, koid, std::move(name), std::move(arch_provider),
                      std::move(object_provider)) {}
  
    ~DebugAgentMockProcess() = default;
  
    void SuspendAndSendModulesIfKnown() override {
      // Send the modules over to the ipc.
      debug_ipc::MessageWriter writer;
      debug_ipc::WriteNotifyModules(modules_to_send_, &writer);
      debug_agent()->stream()->Write(writer.MessageComplete());
    };
  
    void set_modules_to_send(debug_ipc::NotifyModules m) { modules_to_send_ = std::move(m); }
  
   private:
    debug_ipc::NotifyModules modules_to_send_;
  };
  
  class MockLimboProvider : public LimboProvider {
   public:
    MockLimboProvider() : LimboProvider(nullptr) {}
  
    const std::map<zx_koid_t, ProcessExceptionMetadata>& Limbo() const override { return limbo_; }
    const std::vector<zx_koid_t> release_calls() const { return release_calls_; }
  
    bool Valid() const override { return true; }
  
    zx_status_t RetrieveException(zx_koid_t process_koid,
                                  fuchsia::exception::ProcessException* out) override {
      auto it = limbo_.find(process_koid);
      if (it == limbo_.end())
        return ZX_ERR_NOT_FOUND;
  
      *out = ToProcessException(it->second);
  
      limbo_.erase(it);
      return ZX_OK;
    }
  
    void AppendException(const MockProcessObject* process, const MockThreadObject* thread,
                         ExceptionType exception_type) {
      ExceptionInfo info = {};
      info.process_koid = process->koid;
      info.thread_koid = thread->koid;
      info.type = exception_type;
  
      ProcessExceptionMetadata metadata = {};
      metadata.set_info(std::move(info));
      metadata.set_process(process->GetHandle());
      metadata.set_thread(thread->GetHandle());
  
      limbo_[process->koid] = std::move(metadata);
    }
  
    void CallOnEnterLimbo() {
      FXL_DCHECK(on_enter_limbo_);
  
      std::vector<ProcessExceptionMetadata> processes;
      processes.reserve(limbo_.size());
      for (const auto& [process_koid, metadata] : limbo_) {
        processes.push_back(CopyMetadata(metadata));
      }
  
      on_enter_limbo_(std::move(processes));
    }
  
    zx_status_t ReleaseProcess(zx_koid_t process_koid) override {
      release_calls_.push_back(process_koid);
  
      auto it = limbo_.find(process_koid);
      if (it == limbo_.end())
        return ZX_ERR_NOT_FOUND;
  
      limbo_.erase(it);
      return ZX_OK;
    }
  
   private:
    ProcessExceptionMetadata CopyMetadata(const ProcessExceptionMetadata& metadata) {
      ProcessExceptionMetadata exception = {};
  
      exception.set_info(metadata.info());
      exception.set_process(zx::process(metadata.info().process_koid));
      exception.set_thread(zx::thread(metadata.info().thread_koid));
  
      return exception;
    }
  
    ProcessException ToProcessException(const ProcessExceptionMetadata& metadata) {
      ProcessException exception = {};
  
      exception.set_info(metadata.info());
      exception.set_exception(zx::exception(metadata.info().thread_koid));
      exception.set_process(zx::process(metadata.info().process_koid));
      exception.set_thread(zx::thread(metadata.info().thread_koid));
  
      return exception;
    }
  
    std::map<zx_koid_t, ProcessExceptionMetadata> limbo_;
    std::vector<zx_koid_t> release_calls_;
  };
  
  std::pair<const MockProcessObject*, const MockThreadObject*> GetProcessThread(
      const MockObjectProvider& object_provider, const std::string& process_name,
      const std::string& thread_name) {
    const MockProcessObject* process = object_provider.ProcessByName(process_name);
    FXL_DCHECK(process);
    const MockThreadObject* thread = process->GetThread(thread_name);
    FXL_DCHECK(thread);
  
    return {process, thread};
  }
  
  struct TestContext {
    DebugAgentMessageLoop loop;
    DebugAgentStreamBackend stream_backend;
  
    std::shared_ptr<MockObjectProvider> object_provider;
    std::shared_ptr<MockLimboProvider> limbo_provider;
    std::shared_ptr<arch::ArchProvider> arch_provider;
  };
  
  SystemProviders ToSystemProviders(const TestContext& context) {
    SystemProviders providers;
    providers.arch_provider = context.arch_provider;
    providers.limbo_provider = context.limbo_provider;
    providers.object_provider = context.object_provider;
  
    return providers;
  }
  
  std::unique_ptr<TestContext> CreateTestContext() {
    auto context = std::make_unique<TestContext>();
    context->arch_provider = std::make_shared<ArchProviderImpl>();
    context->limbo_provider = std::make_shared<MockLimboProvider>();
    context->object_provider = CreateDefaultMockObjectProvider();
    return context;
  }
  
  // Tests -------------------------------------------------------------------------------------------
  
  TEST(DebugAgent, OnGlobalStatus) {
    auto test_context = CreateTestContext();
  
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    debug_ipc::StatusRequest request = {};
  
    debug_ipc::StatusReply reply = {};
    remote_api->OnStatus(request, &reply);
  
    ASSERT_EQ(reply.processes.size(), 0u);
  
    constexpr uint64_t kProcessKoid1 = 0x1234;
    const std::string kProcessName1 = "process-1";
    constexpr uint64_t kProcess1ThreadKoid1 = 0x1;
  
    auto process1 =
        std::make_unique<MockProcess>(nullptr, kProcessKoid1, kProcessName1,
                                      test_context->arch_provider, test_context->object_provider);
    process1->AddThread(kProcess1ThreadKoid1);
    debug_agent.InjectProcessForTest(std::move(process1));
  
    reply = {};
    remote_api->OnStatus(request, &reply);
  
    ASSERT_EQ(reply.processes.size(), 1u);
    EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
    EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
    ASSERT_EQ(reply.processes[0].threads.size(), 1u);
    EXPECT_EQ(reply.processes[0].threads[0].process_koid, kProcessKoid1);
    EXPECT_EQ(reply.processes[0].threads[0].thread_koid, kProcess1ThreadKoid1);
  
    constexpr uint64_t kProcessKoid2 = 0x5678;
    const std::string kProcessName2 = "process-2";
    constexpr uint64_t kProcess2ThreadKoid1 = 0x1;
    constexpr uint64_t kProcess2ThreadKoid2 = 0x2;
  
    auto process2 =
        std::make_unique<MockProcess>(nullptr, kProcessKoid2, kProcessName2,
                                      test_context->arch_provider, test_context->object_provider);
    process2->AddThread(kProcess2ThreadKoid1);
    process2->AddThread(kProcess2ThreadKoid2);
    debug_agent.InjectProcessForTest(std::move(process2));
  
    reply = {};
    remote_api->OnStatus(request, &reply);
  
    ASSERT_EQ(reply.processes.size(), 2u);
    EXPECT_EQ(reply.processes[0].process_koid, kProcessKoid1);
    EXPECT_EQ(reply.processes[0].process_name, kProcessName1);
    ASSERT_EQ(reply.processes[0].threads.size(), 1u);
    EXPECT_EQ(reply.processes[0].threads[0].process_koid, kProcessKoid1);
    EXPECT_EQ(reply.processes[0].threads[0].thread_koid, kProcess1ThreadKoid1);
  
    EXPECT_EQ(reply.processes[1].process_koid, kProcessKoid2);
    EXPECT_EQ(reply.processes[1].process_name, kProcessName2);
    ASSERT_EQ(reply.processes[1].threads.size(), 2u);
    EXPECT_EQ(reply.processes[1].threads[0].process_koid, kProcessKoid2);
    EXPECT_EQ(reply.processes[1].threads[0].thread_koid, kProcess2ThreadKoid1);
    EXPECT_EQ(reply.processes[1].threads[1].process_koid, kProcessKoid2);
    EXPECT_EQ(reply.processes[1].threads[1].thread_koid, kProcess2ThreadKoid2);
  
    // Set a limbo provider.
    const MockObjectProvider& object_provider = *test_context->object_provider;
  
    const std::string kLimboProcess1 = "job1-p1";
    const std::string kLimboProcess1Thread = "initial-thread";
    constexpr ExceptionType kLimboException1 = ExceptionType::FATAL_PAGE_FAULT;
    auto [limbo_proc1, limbo_thread1] =
        GetProcessThread(object_provider, kLimboProcess1, kLimboProcess1Thread);
  
    const std::string kLimboProcess2 = "job121-p2";
    const std::string kLimboProcess2Thread = "second-thread";
    constexpr ExceptionType kLimboException2 = ExceptionType::UNALIGNED_ACCESS;
    auto [limbo_proc2, limbo_thread2] =
        GetProcessThread(object_provider, kLimboProcess2, kLimboProcess2Thread);
  
    test_context->limbo_provider->AppendException(limbo_proc1, limbo_thread1, kLimboException1);
    test_context->limbo_provider->AppendException(limbo_proc2, limbo_thread2, kLimboException2);
  
    reply = {};
    remote_api->OnStatus(request, &reply);
  
    // The attached processes should still be there.
    ASSERT_EQ(reply.processes.size(), 2u);
  
    // The limbo processes should be there.
    ASSERT_EQ(reply.limbo.size(), 2u);
    EXPECT_EQ(reply.limbo[0].process_koid, limbo_proc1->koid);
    EXPECT_EQ(reply.limbo[0].process_name, limbo_proc1->name);
    ASSERT_EQ(reply.limbo[0].threads.size(), 1u);
    EXPECT_EQ(reply.limbo[0].threads[0].process_koid, limbo_proc1->koid);
    EXPECT_EQ(reply.limbo[0].threads[0].thread_koid, limbo_thread1->koid);
    EXPECT_EQ(reply.limbo[0].threads[0].name, limbo_thread1->name);
    EXPECT_EQ(reply.limbo[0].threads[0].state, debug_ipc::ThreadRecord::State::kBlocked);
    EXPECT_EQ(reply.limbo[0].threads[0].blocked_reason,
              debug_ipc::ThreadRecord::BlockedReason::kException);
  
    // TODO(donosoc): Add exception type.
  }
  
  TEST(DebugAgent, OnProcessStatus) {
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    constexpr uint64_t kProcessKoid1 = 0x1234;
    std::string kProcessName1 = "process-1";
    auto process1 = std::make_unique<DebugAgentMockProcess>(
        &debug_agent, kProcessKoid1, kProcessName1, test_context->object_provider,
        test_context->arch_provider);
    debug_agent.InjectProcessForTest(std::move(process1));
  
    constexpr uint64_t kProcessKoid2 = 0x5678;
    std::string kProcessName2 = "process-2";
    auto process2 = std::make_unique<DebugAgentMockProcess>(
        &debug_agent, kProcessKoid2, kProcessName2, test_context->object_provider,
        test_context->arch_provider);
    auto* process2_ptr = process2.get();
    debug_agent.InjectProcessForTest(std::move(process2));
  
    // Asking for a un-existent process should fail.
    debug_ipc::ProcessStatusRequest request = {};
    request.process_koid = 0xdeadbeef;
  
    debug_ipc::ProcessStatusReply reply = {};
    remote_api->OnProcessStatus(request, &reply);
    EXPECT_EQ(reply.status, (uint32_t)ZX_ERR_NOT_FOUND) << zx_status_get_string(reply.status);
  
    debug_ipc::NotifyModules modules_to_send = {};
    modules_to_send.process_koid = kProcessKoid2;
    modules_to_send.modules.push_back({"module-1", 0x1, "build-1"});
    modules_to_send.modules.push_back({"module-2", 0x2, "build-2"});
    process2_ptr->set_modules_to_send(modules_to_send);
  
    // Asking for an existent one should send the process and modules notification.
    request.process_koid = kProcessKoid2;
    remote_api->OnProcessStatus(request, &reply);
    EXPECT_EQ(reply.status, (uint32_t)ZX_OK) << zx_status_get_string(reply.status);
  
    test_context->loop.RunUntilNoTasks();
  
    auto& process_starts = test_context->stream_backend.process_starts();
    ASSERT_EQ(process_starts.size(), 1u);
    EXPECT_EQ(process_starts[0].koid, kProcessKoid2);
    EXPECT_EQ(process_starts[0].name, kProcessName2);
  
    auto& modules = test_context->stream_backend.modules();
    ASSERT_EQ(modules.size(), 1u);
    EXPECT_EQ(modules[0].process_koid, kProcessKoid2);
  
    ASSERT_EQ(modules[0].modules.size(), modules_to_send.modules.size());
    ASSERT_EQ(modules[0].modules[0].name, modules_to_send.modules[0].name);
    ASSERT_EQ(modules[0].modules[0].base, modules_to_send.modules[0].base);
    ASSERT_EQ(modules[0].modules[0].build_id, modules_to_send.modules[0].build_id);
    ASSERT_EQ(modules[0].modules[1].name, modules_to_send.modules[1].name);
    ASSERT_EQ(modules[0].modules[1].base, modules_to_send.modules[1].base);
    ASSERT_EQ(modules[0].modules[1].build_id, modules_to_send.modules[1].build_id);
  }
  
  TEST(DebugAgent, OnAttachNotFound) {
    uint32_t transaction_id = 1u;
  
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    debug_ipc::AttachRequest attach_request;
    attach_request.type = debug_ipc::TaskType::kProcess;
    attach_request.koid = -1;
  
    remote_api->OnAttach(transaction_id++, attach_request);
  
    {
      // Should've gotten an attach reply.
      auto& attach_replies = test_context->stream_backend.attach_replies();
      ASSERT_EQ(attach_replies.size(), 1u);
      EXPECT_ZX_EQ(attach_replies[0].status, ZX_ERR_NOT_FOUND);
  
      // There should be no attach watch.
      auto& watches = test_context->loop.watches();
      ASSERT_EQ(watches.size(), 0u);
    }
  
    auto [proc_object, thread_object] =
        GetProcessThread(*test_context->object_provider, "job11-p1", "second-thread");
    test_context->limbo_provider->AppendException(proc_object, thread_object,
                                                  ExceptionType::FATAL_PAGE_FAULT);
  
    // Even with limbo it should fail.
    remote_api->OnAttach(transaction_id++, attach_request);
  
    {
      // Should've gotten an attach reply.
      auto& attach_replies = test_context->stream_backend.attach_replies();
      ASSERT_EQ(attach_replies.size(), 2u);
      EXPECT_ZX_EQ(attach_replies[1].status, ZX_ERR_NOT_FOUND);
  
      // There should be no attach watch.
      auto& watches = test_context->loop.watches();
      ASSERT_EQ(watches.size(), 0u);
    }
  }
  
  TEST(DebugAgent, OnAttach) {
    uint32_t transaction_id = 1u;
  
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    debug_ipc::AttachRequest attach_request;
    attach_request.type = debug_ipc::TaskType::kProcess;
    attach_request.koid = 11;
  
    remote_api->OnAttach(transaction_id++, attach_request);
  
    // We should've received a watch command (which does the low level exception watching).
    auto& watches = test_context->loop.watches();
    ASSERT_EQ(watches.size(), 1u);
    EXPECT_EQ(watches[0].process_name, "job1-p2");
    EXPECT_EQ(watches[0].process_handle, 11u);
    EXPECT_EQ(watches[0].process_koid, 11u);
  
    // We should've gotten an attach reply.
    auto& attach_replies = test_context->stream_backend.attach_replies();
    auto reply = attach_replies.back();
    ASSERT_EQ(attach_replies.size(), 1u);
    EXPECT_ZX_EQ(reply.status, ZX_OK);
    EXPECT_EQ(reply.koid, 11u);
    EXPECT_EQ(reply.name, "job1-p2");
  
    // Asking for some invalid process should fail.
    attach_request.koid = 0x231315;  // Some invalid value.
    remote_api->OnAttach(transaction_id++, attach_request);
  
    // We should've gotten an error reply.
    ASSERT_EQ(attach_replies.size(), 2u);
    reply = attach_replies.back();
    EXPECT_ZX_EQ(reply.status, ZX_ERR_NOT_FOUND);
  
    // Attaching to a third process should work.
    attach_request.koid = 21u;
    remote_api->OnAttach(transaction_id++, attach_request);
  
    ASSERT_EQ(attach_replies.size(), 3u);
    reply = attach_replies.back();
    EXPECT_ZX_EQ(reply.status, ZX_OK);
    EXPECT_EQ(reply.koid, 21u);
    EXPECT_EQ(reply.name, "job121-p2");
  
    // Attaching again to a process should fail.
    remote_api->OnAttach(transaction_id++, attach_request);
  
    ASSERT_EQ(attach_replies.size(), 4u);
    reply = attach_replies.back();
    EXPECT_ZX_EQ(reply.status, ZX_ERR_ALREADY_BOUND);
  }
  
  TEST(DebugAgent, AttachToLimbo) {
    uint32_t transaction_id = 1u;
  
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    auto [proc_object, thread_object] =
        GetProcessThread(*test_context->object_provider, "job11-p1", "second-thread");
    test_context->limbo_provider->AppendException(proc_object, thread_object,
                                                  ExceptionType::FATAL_PAGE_FAULT);
  
    debug_ipc::AttachRequest attach_request = {};
    attach_request.type = debug_ipc::TaskType::kProcess;
    attach_request.koid = proc_object->koid;
    remote_api->OnAttach(transaction_id++, attach_request);
  
    // We should've received a watch command (which does the low level exception watching).
    auto& watches = test_context->loop.watches();
    ASSERT_EQ(watches.size(), 1u);
    EXPECT_EQ(watches[0].process_name, proc_object->name);
    EXPECT_EQ(watches[0].process_handle, proc_object->koid);
    EXPECT_EQ(watches[0].process_koid, proc_object->koid);
  
    // We should've gotten an attach reply.
    auto& attach_replies = test_context->stream_backend.attach_replies();
    auto reply = attach_replies.back();
    ASSERT_EQ(attach_replies.size(), 1u);
    EXPECT_ZX_EQ(reply.status, ZX_OK);
    EXPECT_EQ(reply.koid, proc_object->koid);
    EXPECT_EQ(reply.name, proc_object->name);
  
    {
      DebuggedProcess* process = debug_agent.GetDebuggedProcess(proc_object->koid);
      ASSERT_TRUE(process);
      auto threads = process->GetThreads();
      ASSERT_EQ(threads.size(), 2u);
  
      // Search for the exception thread.
      DebuggedThread* exception_thread = nullptr;
      for (DebuggedThread* thread : threads) {
        if (thread->koid() == thread_object->koid) {
          exception_thread = thread;
          break;
        }
      }
  
      ASSERT_TRUE(exception_thread);
      ASSERT_TRUE(exception_thread->IsInException());
      EXPECT_EQ(exception_thread->exception_handle().get(), thread_object->koid);
    }
  }
  
  TEST(DebugAgent, OnEnterLimbo) {
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
  
    auto [proc_object, thread_object] =
        GetProcessThread(*test_context->object_provider, "job11-p1", "second-thread");
    test_context->limbo_provider->AppendException(proc_object, thread_object,
                                                  ExceptionType::FATAL_PAGE_FAULT);
  
    // Call the limbo.
    test_context->limbo_provider->CallOnEnterLimbo();
  
    // Should've sent a notification.
    {
      ASSERT_EQ(test_context->stream_backend.process_starts().size(), 1u);
  
      auto& process_start = test_context->stream_backend.process_starts()[0];
      EXPECT_EQ(process_start.type, debug_ipc::NotifyProcessStarting::Type::kLimbo);
      EXPECT_EQ(process_start.koid, proc_object->koid);
      EXPECT_EQ(process_start.component_id, 0u);
      EXPECT_EQ(process_start.name, proc_object->name);
    }
  }
  
  TEST(DebugAgent, DetachFromLimbo) {
    auto test_context = CreateTestContext();
    DebugAgent debug_agent(nullptr, ToSystemProviders(*test_context));
    debug_agent.Connect(&test_context->stream_backend.stream());
    RemoteAPI* remote_api = &debug_agent;
  
    auto [proc_object, thread_object] =
        GetProcessThread(*test_context->object_provider, "job11-p1", "second-thread");
  
    // Attempting to detach to a process that doesn't exist should fail.
    {
      debug_ipc::DetachRequest request = {};
      request.type = debug_ipc::TaskType::kProcess;
      request.koid = proc_object->koid;
  
      debug_ipc::DetachReply reply = {};
      remote_api->OnDetach(request, &reply);
  
      ASSERT_ZX_EQ(reply.status, ZX_ERR_NOT_FOUND);
      ASSERT_EQ(test_context->limbo_provider->release_calls().size(), 0u);
    }
  
    // Adding it should now find it and remove it.
    test_context->limbo_provider->AppendException(proc_object, thread_object,
                                                  ExceptionType::FATAL_PAGE_FAULT);
    {
      debug_ipc::DetachRequest request = {};
      request.type = debug_ipc::TaskType::kProcess;
      request.koid = proc_object->koid;
  
      debug_ipc::DetachReply reply = {};
      remote_api->OnDetach(request, &reply);
  
      ASSERT_ZX_EQ(reply.status, ZX_OK);
      ASSERT_EQ(test_context->limbo_provider->release_calls().size(), 1u);
      EXPECT_EQ(test_context->limbo_provider->release_calls()[0], proc_object->koid);
    }
  
    // This should've remove it from limbo, trying it again should fail.
    {
      debug_ipc::DetachRequest request = {};
      request.type = debug_ipc::TaskType::kProcess;
      request.koid = proc_object->koid;
  
      debug_ipc::DetachReply reply = {};
      remote_api->OnDetach(request, &reply);
  
      ASSERT_ZX_EQ(reply.status, ZX_ERR_NOT_FOUND);
      ASSERT_EQ(test_context->limbo_provider->release_calls().size(), 1u);
      EXPECT_EQ(test_context->limbo_provider->release_calls()[0], proc_object->koid);
    }
  }
  
  }  // namespace
  }  // namespace debug_agent
  