editor: explicit runtime path contract

This commit is contained in:
2026-04-28 14:49:41 +08:00
parent 0e506f21ec
commit cd166037bf
38 changed files with 210 additions and 129 deletions

View File

@@ -65,7 +65,7 @@ TEST(EditorProjectRuntimeTests, NavigateToFolderClearsCurrentProjectSelection) {
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc"));
@@ -80,7 +80,7 @@ TEST(EditorProjectRuntimeTests, OpenSceneItemQueuesSceneOpenRequest) {
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
ASSERT_TRUE(runtime.OpenItem("Assets/Scenes/Main.xc"));
@@ -97,7 +97,7 @@ TEST(EditorProjectRuntimeTests, ResolveCommandTargetsFollowRuntimeSelectionAndCu
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scenes"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scenes/Main.xc"));
@@ -134,7 +134,7 @@ TEST(EditorProjectRuntimeTests, ResolveEditCommandTargetMarksAssetsRootAndIgnore
TemporaryRepo repo = {};
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
const std::optional<EditorProjectRuntime::EditCommandTarget> rootTarget =
runtime.ResolveEditCommandTarget();
@@ -153,7 +153,7 @@ TEST(EditorProjectRuntimeTests, RenameSelectedItemRemapsSelectionAndDeleteClears
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
@@ -176,7 +176,7 @@ TEST(EditorProjectRuntimeTests, BoundSelectionServiceBecomesTheSingleProjectSele
EditorSelectionService selectionService = {};
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
runtime.BindSelectionService(&selectionService);
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));

View File

@@ -15,6 +15,7 @@ using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
using XCEngine::UI::Editor::App::EditorProductManifestValidationCode;
using XCEngine::UI::Editor::App::EditorProductPanelRuntimeKind;
using XCEngine::UI::Editor::App::EditorProductViewportRendererKind;
using XCEngine::UI::Editor::App::EditorRuntimePaths;
using XCEngine::UI::Editor::App::FindEditorProductPanel;
using XCEngine::UI::Editor::App::GetEditorProductPanels;
using XCEngine::UI::Editor::App::kGamePanelId;
@@ -56,8 +57,18 @@ UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) {
return binding;
}
EditorRuntimePaths TestRuntimePaths() {
return EditorRuntimePaths{
.workspaceRoot = ".",
.executableRoot = ".",
.resourceRoot = "editor/resources",
.projectRoot = "project",
.captureRoot = "captures",
};
}
TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) {
const auto shellAsset = BuildEditorApplicationShellAsset(".");
const auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_TRUE(validation.IsValid()) << validation.message;
@@ -79,7 +90,7 @@ TEST(EditorShellAssetValidationTest, ProductManifestDeclaresPanelRuntimeAndViewp
<< validation.message;
ASSERT_TRUE(validation.IsValid());
const auto shellAsset = BuildEditorApplicationShellAsset(".");
const auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
ASSERT_EQ(shellAsset.panelRegistry.panels.size(), GetEditorProductPanels().size());
const auto* gamePanel = FindEditorProductPanel(kGamePanelId);
@@ -92,7 +103,7 @@ TEST(EditorShellAssetValidationTest, ProductManifestDeclaresPanelRuntimeAndViewp
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
auto* documentPanel =
const_cast<XCEngine::UI::Editor::UIEditorPanelDescriptor*>(
@@ -108,7 +119,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromR
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromRegistry) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
auto* scenePanel = FindWorkspacePanelNode(shellAsset.workspace.root, "scene");
ASSERT_NE(scenePanel, nullptr);
shellAsset.workspace.activePanelId = scenePanel->panel.panelId;
@@ -119,7 +130,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromReg
}
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionState) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
ASSERT_FALSE(shellAsset.workspaceSession.panelStates.empty());
shellAsset.workspaceSession.panelStates.front().open = false;
@@ -128,7 +139,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionSta
}
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFromRegistry) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
@@ -142,7 +153,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFr
}
TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentationPanelId) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
@@ -156,7 +167,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentation
}
TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresentation) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
shellAsset.shellDefinition.workspacePresentations.clear();
const auto validation = ValidateEditorShellAsset(shellAsset);
@@ -166,7 +177,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresen
}
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShellMenuModel) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
shellAsset.shellDefinition.menuModel.menus = {
{
"window",
@@ -191,7 +202,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShellMenuModel) {
}
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShortcutConfiguration) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
shellAsset.shortcutAsset.bindings.push_back(
MakeBinding("missing.command", KeyCode::R));
@@ -202,7 +213,7 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidShortcutConfigurati
}
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationKindMismatch) {
auto shellAsset = BuildEditorApplicationShellAsset(".");
auto shellAsset = BuildEditorApplicationShellAsset(TestRuntimePaths());
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());

View File

@@ -67,7 +67,7 @@ TEST(ProjectBrowserModelTests, ReparentFolderMovesFolderMetaAndRemapsCurrentFold
ASSERT_TRUE(repo.WriteFile("project/Assets/B.meta"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/A/Child"));
@@ -89,7 +89,7 @@ TEST(ProjectBrowserModelTests, MoveFolderToRootMovesFolderMetaAndRemapsCurrentFo
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
@@ -110,7 +110,7 @@ TEST(ProjectBrowserModelTests, CreateFolderCreatesUniqueDirectoryUnderTargetFold
ASSERT_TRUE(repo.CreateDirectory("project/Assets/Scenes/New Folder"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
std::string createdFolderId = {};
ASSERT_TRUE(model.CreateFolder("Assets/Scenes", "New Folder", &createdFolderId));
@@ -125,7 +125,7 @@ TEST(ProjectBrowserModelTests, CreateMaterialCreatesUniqueMaterialFileAndExposes
ASSERT_TRUE(repo.WriteFile("project/Assets/Materials/New Material.mat"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
std::string createdItemId = {};
ASSERT_TRUE(model.CreateMaterial("Assets/Materials", "New Material", &createdItemId));
@@ -151,7 +151,7 @@ TEST(ProjectBrowserModelTests, CanMoveItemToFolderRejectsDescendantFolderTargets
ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderB"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
EXPECT_FALSE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderA/Nested"));
EXPECT_TRUE(model.CanMoveItemToFolder("Assets/FolderA", "Assets/FolderB"));
@@ -165,7 +165,7 @@ TEST(ProjectBrowserModelTests, MoveItemToFolderMovesFileMetaAndRefreshesCurrentL
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Scripts"));
std::string movedItemId = {};
@@ -192,7 +192,7 @@ TEST(ProjectBrowserModelTests, RenameFilePreservesExtensionAndUpdatesCurrentList
ASSERT_TRUE(repo.WriteFile("project/Assets/Scenes/Main.xc.meta"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Scenes"));
std::string renamedItemId = {};
@@ -218,7 +218,7 @@ TEST(ProjectBrowserModelTests, DeleteFolderRemovesMetaAndFallsBackCurrentFolder)
ASSERT_TRUE(repo.WriteFile("project/Assets/Parent/Nested.meta"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
ASSERT_TRUE(model.NavigateToFolder("Assets/Parent/Nested"));
ASSERT_TRUE(model.DeleteItem("Assets/Parent"));
@@ -236,7 +236,7 @@ TEST(ProjectBrowserModelTests, AssetEntriesExposeKindAndOpenCapability) {
ASSERT_TRUE(repo.WriteFile("project/Assets/readme.txt"));
ProjectBrowserModel model = {};
model.Initialize(repo.Root());
model.Initialize(repo.Root() / "project");
const ProjectBrowserModel::AssetEntry* sceneEntry =
model.FindAssetEntry("Assets/mesh.fbx");

View File

@@ -144,7 +144,7 @@ TEST(ProjectPanelTests, CreateFolderCommandCreatesDirectoryAndQueuesRename) {
TemporaryRepo repo = {};
ProjectPanel panel = {};
panel.Initialize(repo.Root());
panel.Initialize(repo.Root() / "project");
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_folder");
@@ -169,7 +169,7 @@ TEST(ProjectPanelTests, CreateMaterialCommandCreatesFileAndQueuesRename) {
TemporaryRepo repo = {};
ProjectPanel panel = {};
panel.Initialize(repo.Root());
panel.Initialize(repo.Root() / "project");
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_material");
@@ -192,7 +192,7 @@ TEST(ProjectPanelTests, BackgroundContextMenuCreateFolderUsesCurrentFolder) {
TemporaryRepo repo = {};
ProjectPanel panel = {};
panel.Initialize(repo.Root());
panel.Initialize(repo.Root() / "project");
const UIEditorHostedPanelDispatchEntry dispatchEntry = MakeProjectDispatchEntry();
panel.Update(
@@ -219,7 +219,7 @@ TEST(ProjectPanelTests, FolderContextMenuCreateFolderUsesFolderTarget) {
ASSERT_TRUE(std::filesystem::create_directories(repo.Root() / "project/Assets/FolderA"));
ProjectPanel panel = {};
panel.Initialize(repo.Root());
panel.Initialize(repo.Root() / "project");
const UIEditorHostedPanelDispatchEntry dispatchEntry = MakeProjectDispatchEntry();
panel.Update(
@@ -239,7 +239,7 @@ TEST(ProjectPanelTests, InjectedRuntimeSelectionDrivesRenameWithoutPanelSelectio
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs.meta"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.SetProjectRuntime(&runtime);
@@ -263,7 +263,7 @@ TEST(ProjectPanelTests, InjectedRuntimeCurrentFolderDrivesRenameFallbackWithoutT
ASSERT_TRUE(repo.CreateDirectory("project/Assets/FolderA"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ProjectPanel panel = {};
panel.SetProjectRuntime(&runtime);
@@ -283,7 +283,7 @@ TEST(ProjectPanelTests, IconServiceCanBeConfiguredBeforeRuntimeInitialization) {
FakeIconService icons = {};
panel.SetIconService(&icons);
panel.Initialize(repo.Root());
panel.Initialize(repo.Root() / "project");
const UIEditorHostCommandEvaluationResult evaluation =
panel.EvaluateAssetCommand("assets.create_folder");
@@ -296,7 +296,7 @@ TEST(ProjectPanelTests, CopyPathCommandUsesInjectedSystemHost) {
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));
@@ -322,7 +322,7 @@ TEST(ProjectPanelTests, ShowInExplorerCommandUsesInjectedSystemHost) {
ASSERT_TRUE(repo.WriteFile("project/Assets/Scripts/Player.cs"));
EditorProjectRuntime runtime = {};
ASSERT_TRUE(runtime.Initialize(repo.Root()));
ASSERT_TRUE(runtime.Initialize(repo.Root() / "project"));
ASSERT_TRUE(runtime.NavigateToFolder("Assets/Scripts"));
ASSERT_TRUE(runtime.SetSelection("Assets/Scripts/Player.cs"));

View File

@@ -20,6 +20,7 @@ namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
using XCEngine::UI::Editor::App::EditorRuntimePaths;
using XCEngine::UI::Editor::BuildStructuredEditorShellBinding;
using XCEngine::UI::Editor::BuildStructuredEditorShellServices;
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionModel;
@@ -40,6 +41,17 @@ std::filesystem::path RepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
EditorRuntimePaths TestRuntimePaths() {
const std::filesystem::path root = RepoRootPath();
return EditorRuntimePaths{
.workspaceRoot = root,
.executableRoot = root,
.resourceRoot = root / "editor" / "resources",
.projectRoot = root / "project",
.captureRoot = root / "build" / "editor" / "captures",
};
}
bool PanelRegistriesMatch(
const XCEngine::UI::Editor::UIEditorPanelRegistry& lhs,
const XCEngine::UI::Editor::UIEditorPanelRegistry& rhs) {
@@ -126,7 +138,7 @@ UIShortcutBinding MakeBinding(std::string commandId, KeyCode keyCode) {
} // namespace
TEST(EditorUIStructuredShellTest, StructuredEditorShellDoesNotRequireRepositoryXCUIDocument) {
const auto shell = BuildEditorApplicationShellAsset(RepoRootPath());
const auto shell = BuildEditorApplicationShellAsset(TestRuntimePaths());
const auto binding = BuildStructuredEditorShellBinding(shell);
ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message;
@@ -136,7 +148,7 @@ TEST(EditorUIStructuredShellTest, StructuredEditorShellDoesNotRequireRepositoryX
}
TEST(EditorUIStructuredShellTest, StructuredShellBindingUsesEditorShellAssetAsSingleSource) {
auto shell = BuildEditorApplicationShellAsset(RepoRootPath());
auto shell = BuildEditorApplicationShellAsset(TestRuntimePaths());
shell.shortcutAsset.commandRegistry.commands = {
{
"workspace.reset_layout",

View File

@@ -7,6 +7,7 @@
namespace {
using XCEngine::UI::Editor::App::BuildEditorApplicationShellAsset;
using XCEngine::UI::Editor::App::EditorRuntimePaths;
using XCEngine::UI::Editor::FindUIEditorPanelDescriptor;
using XCEngine::UI::Editor::UIEditorPanelDescriptor;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
@@ -14,8 +15,15 @@ using XCEngine::UI::Editor::UIEditorPanelRegistryValidationCode;
using XCEngine::UI::Editor::ValidateUIEditorPanelRegistry;
TEST(UIEditorPanelRegistryTest, DefaultRegistryContainsShellDescriptors) {
const EditorRuntimePaths paths{
.workspaceRoot = ".",
.executableRoot = ".",
.resourceRoot = "editor/resources",
.projectRoot = "project",
.captureRoot = "captures",
};
const UIEditorPanelRegistry registry =
BuildEditorApplicationShellAsset(".").panelRegistry;
BuildEditorApplicationShellAsset(paths).panelRegistry;
ASSERT_EQ(registry.panels.size(), 6u);
const UIEditorPanelDescriptor* descriptor =