Files
EVEngine/test/editor_core_v2.cpp
西风逍遥游andCursor 24502babef Nest satellite packages under their host modules.
Keep CMake module names and trim switches, but stop flattening editing/editor and runtime bridges as top-level src/modules directories. Parent scans skip nested facets so runtime-3d can drop authoring by directory leaf.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:56:12 +08:00

195 lines
8.2 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "editor/EditCommand.h"
#include "editor/EditorCommandService.h"
#include "editor/EditorHostProfile.h"
#include "editor/EditorIds.h"
#include "editor/EditorSession.h"
#include "editor/EditorValue.h"
#include "map/level/editing/FieldTargets.h"
#include "map/level/editing/TileBuffer.h"
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
using namespace eve::editor;
using namespace eve::level_editing;
static_assert(!std::is_convertible_v<CommandId, ToolId>);
static_assert(!std::is_convertible_v<TargetId, CommandId>);
TEST_CASE("editor.v2.strong_ids_and_values") {
const CommandId command("scene.object.create");
const CommandId same("scene.object.create");
const CommandId other("scene.object.delete");
CHECK(command == same);
CHECK(command != other);
CHECK_EQ(command.value(), std::string("scene.object.create"));
EditorValue::Object root;
root["name"] = "Station";
root["position"] = EditorValue::Array{1.0, 2.0, 3.0};
root["visible"] = true;
const EditorValue value(std::move(root));
CHECK_EQ(static_cast<int>(value.type()), static_cast<int>(EditorValue::Type::Object));
CHECK(value.isWithinLimits(4, 16, 128));
CHECK(!value.isWithinLimits(1, 16, 128));
CHECK(!value.isWithinLimits(4, 2, 128));
CHECK(!value.isWithinLimits(4, 16, 4));
}
TEST_CASE("editor.v2.idsUseCommonUuidAdapterWithLegacyParse") {
const auto legacy = CommandId::parse("scene.object.create");
REQUIRE(legacy.has_value());
CHECK(!legacy->isCanonicalUuid());
CHECK_EQ(legacy->value(), std::string("scene.object.create"));
const auto projected = canonicalUuid(*legacy);
const auto canonical = CommandId::fromUuid(projected);
CHECK(canonical.isCanonicalUuid());
CHECK_EQ(canonical.uuid(), projected);
CHECK_EQ(canonical.canonicalFormat(), projected.format());
const auto parsed = CommandId::parse("01020304-0506-0708-090a-0b0c0d0e0f10");
REQUIRE(parsed.has_value());
CHECK(parsed->isCanonicalUuid());
CHECK_EQ(parsed->value(), std::string("01020304-0506-0708-090a-0b0c0d0e0f10"));
}
TEST_CASE("editor.v2.host_profile_is_an_execution_boundary") {
const CommandId place("scene.asset.place");
const CommandId importAsset("asset.import");
HostProfile runtime = HostProfile::runtimeBuilder();
CHECK(!runtime.allowsCommand(place));
runtime.allowCommand(place);
CHECK(runtime.allowsCommand(place));
CHECK(!runtime.allowsCommand(importAsset));
CHECK(runtime.hasFeatures(HostFeature::RuntimeWorld));
CHECK(!runtime.hasFeatures(HostFeature::SourceAssets));
HostProfile developer = HostProfile::developer();
CHECK(developer.allowsCommand(importAsset));
CHECK(developer.hasFeatures(HostFeature::SourceAssets | HostFeature::BuildCook));
developer.denyCommand(importAsset);
CHECK(!developer.allowsCommand(importAsset));
}
TEST_CASE("editor.v2.command_registry_filters_and_executes") {
EditorCommandService service;
CommandDescriptor descriptor;
descriptor.id = CommandId("scene.rename");
descriptor.ownerModule = "scene";
descriptor.displayName = "Rename";
descriptor.requiredFeatures = HostFeature::RuntimeWorld;
int calls = 0;
auto registered = service.registerCommand(descriptor, [&](const CommandContext&, const EditorValue& payload) {
++calls;
return eve::editing::applied<EditorValue>(payload);
});
CHECK(registered.ok());
CHECK_EQ(service.revision(), static_cast<uint64_t>(1));
auto duplicate = service.registerCommand(descriptor, [](const CommandContext&, const EditorValue&) {
return eve::editing::applied<EditorValue>(EditorValue{});
});
CHECK_EQ(static_cast<int>(duplicate.code()), static_cast<int>(EditorStatus::Rejected));
HostProfile runtime = HostProfile::runtimeBuilder();
CommandContext context;
context.profile = &runtime;
auto denied = service.execute(descriptor.id, context, EditorValue("New Name"));
CHECK_EQ(static_cast<int>(denied.code()), static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(calls, 0);
runtime.allowCommand(descriptor.id);
auto executed = service.execute(descriptor.id, context, EditorValue("New Name"));
CHECK(executed.ok());
CHECK_EQ(calls, 1);
CHECK(executed.ok());
CHECK_EQ(*executed.value().getIf<std::string>(), std::string("New Name"));
CHECK_EQ(service.commands(runtime).size(), static_cast<size_t>(1));
CHECK_EQ(service.unregisterOwner("scene"), static_cast<size_t>(1));
CHECK(service.find(descriptor.id) == nullptr);
}
TEST_CASE("editor.v2.command_registry_gates_automation_and_exceptions") {
EditorCommandService service;
CommandDescriptor descriptor;
descriptor.id = CommandId("scene.dangerous");
descriptor.ownerModule = "scene";
descriptor.automationAllowed = false;
CHECK(service
.registerCommand(descriptor,
[](const CommandContext&, const EditorValue&) {
throw std::runtime_error("boom");
return eve::editing::applied<EditorValue>(EditorValue{});
})
.ok());
HostProfile developer = HostProfile::developer();
CommandContext context;
context.profile = &developer;
context.source = CommandSource::Automation;
auto automation = service.execute(descriptor.id, context, {});
CHECK_EQ(static_cast<int>(automation.code()), static_cast<int>(EditorStatus::Rejected));
context.source = CommandSource::Api;
auto failure = service.execute(descriptor.id, context, {});
CHECK_EQ(static_cast<int>(failure.code()), static_cast<int>(EditorStatus::Failed));
CHECK_EQ(failure.diagnostics().size(), static_cast<size_t>(1));
CHECK(failure.diagnostics().front().message().find("boom") != std::string::npos);
}
TEST_CASE("editor.v2.session_wraps_command_in_legacy_transaction") {
TileBuffer buffer(2, 2);
TileBufferTarget target("tiles", &buffer);
EditorCommandService service;
CommandDescriptor descriptor;
descriptor.id = CommandId("map.paint-one");
descriptor.ownerModule = "map";
descriptor.displayName = "Paint One Tile";
descriptor.requiredFeatures = HostFeature::RuntimeWorld;
CHECK(service
.registerCommand(
descriptor,
[](const CommandContext& context, const EditorValue& payload) {
auto* after = payload.getIf<int64_t>();
if (!context.session || !after)
return eve::editing::failed<EditorValue>(EditorStatus::Rejected, RuleId("map.paint.invalid"),
"Paint command requires an integer value");
auto command = std::make_unique<IntFieldEditCommand>("Paint One Tile", context.session->target());
if (!command->record(0, 0, static_cast<int>(*after)) ||
!context.session->execute(std::move(command)))
return eve::editing::failed<EditorValue>(EditorStatus::Failed, RuleId("map.paint.failed"),
"Tile edit could not be applied");
return eve::editing::applied<EditorValue>(payload);
})
.ok());
EditorSession session;
HostProfile runtime = HostProfile::runtimeBuilder();
runtime.allowCommand(descriptor.id);
session.setHostProfile(std::move(runtime));
session.setCommandService(&service);
session.bindTarget(&target);
auto result = session.executeCommand(descriptor.id, EditorValue(7));
CHECK(result.ok());
CHECK_EQ(buffer.getGid(0, 0), 7);
CHECK(session.transactions().canUndo());
CHECK(session.transactions().undo());
CHECK_EQ(buffer.getGid(0, 0), 0);
session.setHostProfile(HostProfile::runtimeBuilder());
auto denied = session.executeCommand(descriptor.id, EditorValue(9));
CHECK_EQ(static_cast<int>(denied.code()), static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(buffer.getGid(0, 0), 0);
}