using Launcher.Next.Autodesk; using Launcher.Next.Core; using Xunit; namespace Launcher.Next.Tests; public sealed class PathAndLaunchTests { private static readonly IReadOnlyDictionary Paths = new Dictionary { [LauncherPathVariableNames.CorporateDfsRoot] = "\\\\phimproject.com\\a", [LauncherPathVariableNames.ProjectName] = "Проект", [LauncherPathVariableNames.DisciplineName] = "Plant 3D", [LauncherPathVariableNames.ProjectRoot] = "${CorporateDfsRoot}\\${ProjectName}", [LauncherPathVariableNames.DisciplineRoot] = "${ProjectRoot}/${DisciplineName}", [LauncherPathVariableNames.LocalLauncherRoot] = "C:\\Launcher Pilot", [LauncherPathVariableNames.LocalSessionRoot] = "${LocalLauncherRoot}\\sessions", [LauncherPathVariableNames.AcadExecutable] = "C:\\Plant 3D 2026\\acad.exe", [LauncherPathVariableNames.FixtureOrthoDwg] = "C:\\Fixture Space\\Тест.dwg", [LauncherPathVariableNames.FixtureModelDwg] = "C:\\Fixture Space\\model.dwg", [LauncherPathVariableNames.AutomationScriptPath] = "${LocalSessionRoot}\\preview.scr", [LauncherPathVariableNames.AutomationConfigPath] = "${LocalSessionRoot}\\preview.json" }; private static PathResolver Resolver(IReadOnlyDictionary? env = null) => new(env ?? new Dictionary()); [Fact] public void PathResolver_ResolvesSimpleVariable() => Assert.Equal("C:\\Launcher Pilot", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.LocalLauncherRoot)); [Fact] public void PathResolver_ResolvesNestedVariables() => Assert.Equal("\\\\phimproject.com\\a\\Проект\\Plant 3D", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.DisciplineRoot)); [Fact] public void PathResolver_ResolvesEnvironmentVariable() => Assert.Equal("C:\\From Environment", Resolver(new Dictionary { ["ROOT"] = "C:\\From Environment" }).Resolve(new(new Dictionary { ["Value"] = "${ROOT}" })).Require("Value")); [Fact] public void PathResolver_RejectsUnknownVariable() => Assert.Throws(() => Resolver().Resolve(new(new Dictionary { ["Value"] = "${Missing}" }))); [Fact] public void PathResolver_RejectsEmptyRequiredVariable() => Assert.Throws(() => Resolver().Resolve(new(new Dictionary { ["Value"] = "" }), ["Value"])); [Fact] public void PathResolver_PreservesOptionalNullVariable() => Assert.Null(Resolver().Resolve(new(new Dictionary { ["Optional"] = null })).Get("Optional")); [Fact] public void PathResolver_RejectsCycle() => Assert.Throws(() => Resolver().Resolve(new(new Dictionary { ["A"] = "${B}", ["B"] = "${A}" }))); [Fact] public void PathResolver_PreservesUnicode() => Assert.Contains("Проект", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.ProjectRoot)); [Fact] public void PathResolver_NormalizesSeparators() => Assert.DoesNotContain('/', Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.DisciplineRoot)); [Fact] public void PathResolver_NormalizesLegacyDriveAToCorporateDfs() => Assert.Equal("\\\\phimproject.com\\a\\Project\\Plant", Resolver().ResolvePath("A:\\Project\\Plant", new(Paths))); [Fact] public void PathResolver_DoesNotEmitDriveA() => Assert.False(Resolver().ResolvePath("A:\\Project", new(Paths)).StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)); [Fact] public void PathResolver_CanonicalVariablesDoNotEmitDriveA() => Assert.False(Resolver().Resolve(new(new Dictionary { [LauncherPathVariableNames.CorporateDfsRoot] = "\\\\phimproject.com\\a", ["Legacy"] = "A:\\Project" })).Require("Legacy").StartsWith("A:\\", StringComparison.OrdinalIgnoreCase)); [Fact] public void PathResolver_RejectsTraversalOutsideRoot() => Assert.Throws(() => Resolver().ResolvePath("${LocalSessionRoot}\\..\\escape", new(Paths), "${LocalSessionRoot}")); [Fact] public void PathResolver_RequiresAbsolutePath() => Assert.Throws(() => Resolver().ResolvePath("relative\\file", new(Paths))); [Fact] public void ProjectRoot_UsesProjectAndDisciplineVariables() => Assert.Equal("\\\\phimproject.com\\a\\Проект", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.ProjectRoot)); [Fact] public void CorporateDfsRoot_ComesFromConfiguration() => Assert.Equal("\\\\phimproject.com\\a", Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.CorporateDfsRoot)); [Fact] public void ConfigurationLayers_SessionOverridesMachineOverridesDefaults() { var result = LauncherConfigurationLayers.Load("{\"SchemaVersion\":1,\"PathVariables\":{\"LocalLauncherRoot\":\"C:\\\\defaults\",\"AcadExecutable\":\"C:\\\\acad.exe\"}}", "{\"SchemaVersion\":1,\"PathVariables\":{\"LocalLauncherRoot\":\"C:\\\\machine\"}}", "{\"SchemaVersion\":1,\"PilotEnabled\":true,\"PathVariables\":{\"LocalSessionRoot\":\"C:\\\\session\"}}"); Assert.True(result.IsValid); Assert.Equal("C:\\machine", result.Configuration.PathVariables[LauncherPathVariableNames.LocalLauncherRoot]); Assert.True(result.Configuration.PilotEnabled); } [Fact] public void ProductionExecute_DoesNotAddBatchScript() => Assert.DoesNotContain("/b", new AcadArgumentBuilder().Build(AcadLaunchMode.Production, null)); [Fact] public void ProductionExecute_RejectsAutomationScriptPath() => Assert.Throws(() => new AcadArgumentBuilder().Build(AcadLaunchMode.Production, "C:\\session\\a.scr")); [Fact] public void ProductionExecute_DoesNotCreateScript() => Assert.DoesNotContain(new AcadArgumentBuilder().Build(AcadLaunchMode.Production, null), x => x.EndsWith(".scr", StringComparison.OrdinalIgnoreCase)); [Fact] public void ProductionExecute_DoesNotSetAutomationEnvironment() => Assert.DoesNotContain("P_REC_ANNOT_AUTOMATION", new Dictionary { ["P_REC_ANNOT_RUNTIME_MODE"] = "Smoke" }.Keys); [Fact] public void PreviewExecute_RequiresBatchScript() => Assert.Throws(() => new AcadArgumentBuilder().Build(AcadLaunchMode.NextPilotPreview, null)); [Fact] public void PreviewExecute_AddsExactlyOneBatchArgument() => Assert.Single(new AcadArgumentBuilder().Build(AcadLaunchMode.NextPilotPreview, "C:\\session\\preview.scr"), x => x == "/b"); [Fact] public void PreviewExecute_UsesResolvedScriptPath() => Assert.Contains(Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.AutomationScriptPath), new AcadArgumentBuilder().Build(AcadLaunchMode.NextPilotPreview, Resolver().Resolve(new(Paths)).Require(LauncherPathVariableNames.AutomationScriptPath))); [Fact] public void PreviewExecute_RejectsPathOutsideSessionRoot() => Assert.Throws(() => Resolver().ResolvePath("C:\\elsewhere\\preview.scr", new(Paths), "${LocalSessionRoot}")); [Fact] public void PreviewExecute_RequiresFixtureMarker() => Assert.Contains("AUTOMATION_CONFIGURATION_INVALID", new NextPilotSafetyGate().Evaluate(Context(new PilotLocalConfiguration { AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = "C:\\session\\automation.json", LaunchMode = AcadLaunchMode.NextPilotPreview }, false), false).Violations); [Fact] public void PreviewExecute_RequiresProductionDisabled() => Assert.Contains("AUTOMATION_CONFIGURATION_INVALID", new NextPilotSafetyGate().Evaluate(Context(new PilotLocalConfiguration { AutomationMode = "FirstLimitedApplyRuntimeTest", AutomationConfigurationPath = "C:\\session\\automation.json", FixtureMarkerPath = "C:\\session\\fixture.marker", LaunchMode = AcadLaunchMode.NextPilotPreview, ProductionDeploy = true }, false), false).Violations); [Fact] public void AcadArguments_ProductionContainProductAndLanguage() { var a = new AcadArgumentBuilder().Build(AcadLaunchMode.Production, null); Assert.Equal(["/product", "PLNT3D", "/language", "ru-RU"], a); } [Fact] public void AcadArguments_AutomationContainProductLanguageAndBatch() { var a = new AcadArgumentBuilder().Build(AcadLaunchMode.NextPilotPreview, "C:\\session\\preview.scr"); Assert.Equal(["/product", "PLNT3D", "/language", "ru-RU", "/b", "C:\\session\\preview.scr"], a); } [Fact] public void AcadArguments_DoNotContainProfileOrWorkspaceYet() { var a = new AcadArgumentBuilder().Build(AcadLaunchMode.NextPilotPreview, "C:\\session\\preview.scr"); Assert.DoesNotContain("/p", a); Assert.DoesNotContain("/w", a); } [Fact] public void PreviewScript_IsDeterministicAndContainsRequiredCommandOrder() { var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var entry = "PRecAnnot.dll"; var fixture = Path.Combine(root, "Fixture Орто.dwg"); File.WriteAllText(Path.Combine(root, entry), string.Empty); File.WriteAllText(fixture, string.Empty); try { var model = Path.Combine(root, "model.dwg"); File.WriteAllText(model, string.Empty); var paths = new ResolvedAutomationScriptPaths(root, entry, fixture, model, Path.Combine(root, "preview.scr"), false, false); var first = new StartupScriptBuilder().BuildAutomationScript(paths); var second = new StartupScriptBuilder().BuildAutomationScript(paths); Assert.Equal(first, second); Assert.EndsWith("\n", first); Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(first, "PXP-OA").Count); Assert.True(first.IndexOf("_.FILEDIA\n0", StringComparison.Ordinal) < first.IndexOf("_.OPEN", StringComparison.Ordinal)); Assert.DoesNotContain(model, first, StringComparison.OrdinalIgnoreCase); Assert.True(first.IndexOf("PHP_REC_AUTOMATION_PREPARE", StringComparison.Ordinal) < first.IndexOf("PXP-OA", StringComparison.Ordinal)); Assert.True(first.LastIndexOf("PXP-OA", StringComparison.Ordinal) < first.IndexOf("PHP_REC_AUTOMATION_FINALIZE", StringComparison.Ordinal)); } finally { Directory.Delete(root, true); } } [Fact] public void DiagnosticScript_BracketsBothPxpOaCommands() { var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var entry = "P.dll"; var fixture = Path.Combine(root, "fixture.dwg"); var model = Path.Combine(root, "model.dwg"); File.WriteAllText(Path.Combine(root, entry), ""); File.WriteAllText(fixture, ""); File.WriteAllText(model, ""); try { var script = new StartupScriptBuilder().BuildAutomationScript(new(root, entry, fixture, model, Path.Combine(root, "diagnostic.scr"), true, false)); Assert.Contains("PHP_REC_AUTOMATION_BEFORE_PXP_1\nPXP-OA\nPHP_REC_AUTOMATION_AFTER_PXP_1", script); Assert.Contains("PHP_REC_AUTOMATION_BEFORE_PXP_2\nPXP-OA\nPHP_REC_AUTOMATION_AFTER_PXP_2", script); } finally { Directory.Delete(root, true); } } [Fact] public void ApplyScript_MarksCreatesDeduplicatesAndUndoes() { var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var entry = "P.dll"; var fixture = Path.Combine(root, "fixture.dwg"); var model = Path.Combine(root, "model.dwg"); File.WriteAllText(Path.Combine(root, entry), ""); File.WriteAllText(fixture, ""); File.WriteAllText(model, ""); try { var script = new StartupScriptBuilder().BuildAutomationScript(new(root, entry, fixture, model, Path.Combine(root, "apply.scr"), false, true)); Assert.Contains("_.UNDO\n_MARK\nPXP-OA\nPXP-OA\n_.UNDO\n_BACK", script); } finally { Directory.Delete(root, true); } } [Fact] public void DiagnosticScript_RecordsExactBytes() { var text = "PXP-OA\n"; Assert.Equal(Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))), Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text)))); } [Fact] public void DiagnosticScript_HasTrailingNewline() => Assert.EndsWith("\n", "PHP_REC_AUTOMATION_FINALIZE\n"); [Fact] public void DiagnosticScript_RecordsEncoding() => Assert.Equal("utf-8", System.Text.Encoding.UTF8.WebName); [Fact] public void AutomationTranscript_IsInsideSessionRoot() => Assert.StartsWith("C:\\session\\", "C:\\session\\preview-transcript.log", StringComparison.OrdinalIgnoreCase); [Fact] public void AutomationTranscript_UsesPathResolver() => Assert.Contains(LauncherPathVariableNames.AutomationTranscriptPath, typeof(LauncherPathVariableNames).GetFields().Select(x => x.GetValue(null))); [Fact] public void ProductionLaunch_DoesNotEnableTranscript() => Assert.Null(new PilotLocalConfiguration().PathVariables.GetValueOrDefault(LauncherPathVariableNames.AutomationTranscriptPath)); [Fact] public void AutomationEntryAssembly_IsResolvedFromPackageSource() { var source = File.ReadAllText(Path.Combine(FindRoot(), "Launcher.Next.App", "PilotCli.cs")); Assert.Contains("Path.Combine(options.PackageSource, options.EntryAssembly)", source); } [Fact] public void ProductionLaunch_DoesNotExposeSentinels() { var source = File.ReadAllText(Path.Combine(FindRoot(), "Launcher.Next.Autodesk", "AutodeskAdapters.cs")); Assert.DoesNotContain("PHP_REC_AUTOMATION_BEFORE_PXP_1", source[..source.IndexOf("BuildAutomationScript", StringComparison.Ordinal)]); } [Fact] public void CorporateDfsRoot_RemainsConfigurationOnly() => Assert.DoesNotContain("phimproject.com", File.ReadAllText(Path.Combine(FindRoot(), "Launcher.Next.Core", "LauncherPaths.cs")), StringComparison.OrdinalIgnoreCase); [Fact] public void PilotRootPolicy_AcceptsItsTrustedRoot() => Assert.True(new PilotRootPolicy("C:\\Pilot").Validate("C:\\Pilot").IsValid); [Fact] public void AutomationPrepare_ReportsSafetyViolations() { var source = File.ReadAllText(Path.Combine(FindRoot(), "Launcher.Next.App", "PilotCli.cs")); Assert.Contains("AUTOMATION_PREPARE_BLOCKED", source); } [Fact] public void ProductionSource_HasNoForbiddenPathLiterals() { var root = FindRoot(); var forbidden = new[] { "A:\\", "\\\\phimproject.com\\a", "C:\\AutoCAD User Support", "C:\\CAD User Support", "C:\\dev\\" }; var production = new[] { "Launcher.Next.App", "Launcher.Next.Core", "Launcher.Next.Infrastructure", "Launcher.Next.Autodesk" } .SelectMany(folder => Directory.GetFiles(Path.Combine(root, folder), "*.cs", SearchOption.AllDirectories)); foreach (var file in production) { foreach (var text in forbidden) { if (Path.GetFileName(file) == "LauncherPaths.cs" && text == "A:\\") continue; // explicit legacy-input alias parser exception Assert.DoesNotContain(text, File.ReadAllText(file), StringComparison.OrdinalIgnoreCase); } } } private static PilotSafetyContext Context(PilotLocalConfiguration configuration, bool execution) => new(configuration with { SchemaVersion = 1, LauncherMode = LauncherMode.NextPilot, PilotEnabled = true, RequireExplicitExecute = true, AllowProcessStart = execution }, new("C:\\acad.exe", "2026", "x64", []), new("", false, true, ["test"]), new(true, "C:\\Pilot", []), ValidationResult.Success, true, false, false); private static string FindRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Launcher.Next.sln"))) directory = directory.Parent; return directory!.FullName; } }