fix: defer Plant automation to first post-startup idle
This commit is contained in:
@@ -10,10 +10,23 @@ namespace PRecAnnot.Plant3D2026;
|
|||||||
|
|
||||||
public sealed class EntryPoint : IExtensionApplication
|
public sealed class EntryPoint : IExtensionApplication
|
||||||
{
|
{
|
||||||
|
private static bool automationQueued;
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
||||||
RibbonIntegration.Register();
|
RibbonIntegration.Register();
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal) && !automationQueued)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
automationQueued = true;
|
||||||
|
FirstLimitedApplyAutomationBootstrap.Prepare();
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("AutomationBootstrapPreparationFailed", "ERROR", resultStatus: "AUTOMATION_BOOTSTRAP_FAILED", exception: exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +46,9 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
[CommandMethod("PXP-OA")]
|
[CommandMethod("PXP-OA")]
|
||||||
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
|
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
|
||||||
|
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
|
||||||
|
|
||||||
private static void RunDiagnostic(string commandName)
|
private static void RunDiagnostic(string commandName)
|
||||||
{
|
{
|
||||||
var operationId = Guid.NewGuid().ToString("N");
|
var operationId = Guid.NewGuid().ToString("N");
|
||||||
@@ -80,6 +96,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
||||||
? FirstLimitedApplyPermitReader.ReadFor(document)
|
? FirstLimitedApplyPermitReader.ReadFor(document)
|
||||||
: null;
|
: null;
|
||||||
|
var automationTarget = FirstLimitedApplyAutomationCoordinator.SelectedTargetSnapshotId;
|
||||||
|
var limitedTarget = applyPermit?.TargetSnapshotId ?? automationTarget;
|
||||||
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
||||||
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
||||||
|
|
||||||
@@ -95,8 +113,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
CommandName = commandName,
|
CommandName = commandName,
|
||||||
Viewport = viewport,
|
Viewport = viewport,
|
||||||
Objects = objects,
|
Objects = objects,
|
||||||
LimitedApplyTargetSnapshotId = applyPermit?.TargetSnapshotId,
|
LimitedApplyTargetSnapshotId = limitedTarget,
|
||||||
LimitedApplyRuleId = applyPermit is null ? null : FirstLimitedApplyPlanner.RuleId
|
LimitedApplyRuleId = limitedTarget is null ? null : FirstLimitedApplyPlanner.RuleId
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +159,13 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
details["PlanConflictCount"] = summary.ConflictCount;
|
details["PlanConflictCount"] = summary.ConflictCount;
|
||||||
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
||||||
details["ErrorCount"] = summary.ErrorCount;
|
details["ErrorCount"] = summary.ErrorCount;
|
||||||
|
if (string.Equals(commandName, "PXP-OA", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var candidates = objectsByViewport.SelectMany(x => x)
|
||||||
|
.Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null)
|
||||||
|
.Select(x => x.SnapshotId).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
FirstLimitedApplyAutomationCoordinator.RecordPxpOa(planId, candidates, context.Viewports.SingleOrDefault()?.Id, summary.CreateCount, summary.SkipCount, mode == PRecAnnotExecutionMode.Apply ? applyPermit is not null : null);
|
||||||
|
}
|
||||||
if (mode == PRecAnnotExecutionMode.Apply)
|
if (mode == PRecAnnotExecutionMode.Apply)
|
||||||
{
|
{
|
||||||
var corePermit = new FirstLimitedApplyPermit
|
var corePermit = new FirstLimitedApplyPermit
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Runtime;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Test-only coordinator. It is not registered in Ribbon and is inert unless
|
||||||
|
// the explicit NextPilot automation environment and local marker are present.
|
||||||
|
internal sealed record FirstApplyAutomationConfiguration
|
||||||
|
{
|
||||||
|
public required string Phase { get; init; }
|
||||||
|
public string? OrthoDrawingPath { get; init; }
|
||||||
|
public string? ModelDrawingPath { get; init; }
|
||||||
|
public required string EvidencePath { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record AutomationEntityCounts(int Total, int PipeBlocks, int PxpOaXdata, int Owned);
|
||||||
|
|
||||||
|
internal static class FirstLimitedApplyAutomationBootstrap
|
||||||
|
{
|
||||||
|
private enum BootstrapState { Created, Prepared, Armed, Starting, Completed }
|
||||||
|
private static BootstrapState state;
|
||||||
|
private static bool observedIdle;
|
||||||
|
private static int idleCallbackCount;
|
||||||
|
private static int coordinatorStartCount;
|
||||||
|
|
||||||
|
internal static int IdleCallbackCount => idleCallbackCount;
|
||||||
|
internal static int CoordinatorStartCount => coordinatorStartCount;
|
||||||
|
|
||||||
|
// Called from Initialize: BCL gates and one lightweight subscription only.
|
||||||
|
internal static void Prepare()
|
||||||
|
{
|
||||||
|
if (state != BootstrapState.Created) return;
|
||||||
|
state = BootstrapState.Prepared;
|
||||||
|
AcadApplication.Idle += OnIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public test command is intentionally side-effect free with respect to Plant APIs.
|
||||||
|
internal static void Arm()
|
||||||
|
{
|
||||||
|
if (state is BootstrapState.Prepared) state = BootstrapState.Armed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnIdle(object? sender, EventArgs args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
idleCallbackCount++;
|
||||||
|
// The script engine can consume the custom command after NETLOAD. The
|
||||||
|
// first idle is therefore the explicit post-NETLOAD arming point.
|
||||||
|
if (state == BootstrapState.Prepared) { observedIdle = true; state = BootstrapState.Armed; }
|
||||||
|
if (!observedIdle || state != BootstrapState.Armed) return;
|
||||||
|
state = BootstrapState.Starting;
|
||||||
|
AcadApplication.Idle -= OnIdle;
|
||||||
|
coordinatorStartCount++;
|
||||||
|
new FirstLimitedApplyAutomationCoordinator().Start();
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
state = BootstrapState.Completed;
|
||||||
|
AcadApplication.Idle -= OnIdle;
|
||||||
|
FirstLimitedApplyAutomationCoordinator.WriteBootstrapFailure(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal sealed record FirstApplyAutomationEvidence
|
||||||
|
{
|
||||||
|
public int SchemaVersion { get; init; } = 1;
|
||||||
|
public required string Phase { get; init; }
|
||||||
|
public bool Completed { get; init; }
|
||||||
|
public string? ErrorCode { get; init; }
|
||||||
|
public int? DbmodBefore { get; init; }
|
||||||
|
public int? DbmodAfterCommand1 { get; init; }
|
||||||
|
public int? DbmodAfterCommand2 { get; init; }
|
||||||
|
public int? DbmodAfterUndo { get; init; }
|
||||||
|
public AutomationEntityCounts? Baseline { get; init; }
|
||||||
|
public AutomationEntityCounts? AfterCommand1 { get; init; }
|
||||||
|
public AutomationEntityCounts? AfterCommand2 { get; init; }
|
||||||
|
public AutomationEntityCounts? AfterUndo { get; init; }
|
||||||
|
public IReadOnlyList<string> CompletedCommands { get; init; } = Array.Empty<string>();
|
||||||
|
public string? PlanIdRun1 { get; init; }
|
||||||
|
public string? PlanIdRun2 { get; init; }
|
||||||
|
public bool? PlanIdsEqual { get; init; }
|
||||||
|
public int? EligibleCandidateCount { get; init; }
|
||||||
|
public string? SelectedCandidateHash { get; init; }
|
||||||
|
public string? ViewHash { get; init; }
|
||||||
|
public int? CreateRun1 { get; init; }
|
||||||
|
public int? CreateRun2 { get; init; }
|
||||||
|
public int? SkipRun2 { get; init; }
|
||||||
|
public bool? PermitAccepted { get; init; }
|
||||||
|
public int? IdleCallbackCount { get; init; }
|
||||||
|
public int? CoordinatorStartCount { get; init; }
|
||||||
|
public IReadOnlyList<string> StateTransitions { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||||
|
{
|
||||||
|
private sealed record PxpResult(string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted);
|
||||||
|
private static string? selectedTargetSnapshotId;
|
||||||
|
private static readonly List<PxpResult> pxpResults = [];
|
||||||
|
internal static string? SelectedTargetSnapshotId => selectedTargetSnapshotId;
|
||||||
|
internal static void WriteBootstrapFailure(System.Exception exception) =>
|
||||||
|
RuntimeDiagnostics.Write("AutomationBootstrapFailed", "ERROR", resultStatus: "AUTOMATION_BOOTSTRAP_FAILED", exception: exception);
|
||||||
|
internal static void RecordPxpOa(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal))
|
||||||
|
pxpResults.Add(new(planId, candidates, view, create, skip, permitAccepted));
|
||||||
|
}
|
||||||
|
private FirstApplyAutomationConfiguration config = null!;
|
||||||
|
private Document document = null!;
|
||||||
|
private readonly List<string> completed = [];
|
||||||
|
private int step;
|
||||||
|
private int? before, after1, after2, afterUndo;
|
||||||
|
private AutomationEntityCounts? baseline, counts1, counts2, countsUndo;
|
||||||
|
private readonly List<string> transitions = ["Created"];
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
config = ReadConfiguration();
|
||||||
|
transitions.Add("PostStartupReady");
|
||||||
|
if (string.Equals(config.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
CompleteLifecycleProbe();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED");
|
||||||
|
transitions.Add("ProjectActivationStarted");
|
||||||
|
var projectXml = Path.Combine(Path.GetDirectoryName(config.FixtureMarkerPath)!, "Project.xml");
|
||||||
|
var project = PlantProject.LoadProject(projectXml, false, string.Empty, string.Empty);
|
||||||
|
PlantApplication.SetCurrentProject(project, true);
|
||||||
|
transitions.Add("ProjectActivated");
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.ModelDrawingPath)) AcadApplication.DocumentManager.Open(config.ModelDrawingPath, false);
|
||||||
|
transitions.Add("DocumentOpenStarted");
|
||||||
|
document = AcadApplication.DocumentManager.Open(config.OrthoDrawingPath, false);
|
||||||
|
AcadApplication.DocumentManager.MdiActiveDocument = document;
|
||||||
|
transitions.Add("DocumentOpened");
|
||||||
|
document.CommandEnded += OnCommandEnded;
|
||||||
|
document.CommandFailed += OnCommandFailed;
|
||||||
|
document.CommandCancelled += OnCommandCancelled;
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated += OnDocumentActivated;
|
||||||
|
AcadApplication.Idle += OnIdle;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { Complete(false, "AUTOMATION_START_FAILED:" + ex.GetType().Name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteLifecycleProbe()
|
||||||
|
{
|
||||||
|
transitions.Add("LifecycleProbeCompleted");
|
||||||
|
WriteEvidence(true, null);
|
||||||
|
transitions.Add("ExitRequested");
|
||||||
|
AcadApplication.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIdle(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
BeginContextVerification();
|
||||||
|
}
|
||||||
|
private void OnDocumentActivated(object sender, DocumentCollectionEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.Equals(e.Document.Name, document.Name, StringComparison.OrdinalIgnoreCase)) BeginContextVerification();
|
||||||
|
}
|
||||||
|
private void BeginContextVerification()
|
||||||
|
{
|
||||||
|
AcadApplication.Idle -= OnIdle;
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
transitions.Add("ContextVerificationStarted");
|
||||||
|
var context = new PlantRuntimeContextReader().ReadCurrentContext(document);
|
||||||
|
if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1) { Complete(false, $"AUTOMATION_ORTHO_CONTEXT_INVALID:P={context.HasPlantProject};O={context.IsOrthoDrawing};V={context.Viewports.Count}"); return; }
|
||||||
|
var candidates = new PlantObjectReader().ReadObjects(context.Viewports.Single(), CancellationToken.None)
|
||||||
|
.Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null)
|
||||||
|
.Select(x => x.SnapshotId).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
if (candidates.Length == 0) { Complete(false, "AUTOMATION_NO_ELIGIBLE_CANDIDATE"); return; }
|
||||||
|
selectedTargetSnapshotId = candidates[0];
|
||||||
|
transitions.Add("ContextVerified");
|
||||||
|
before = Dbmod(); baseline = Counts();
|
||||||
|
Queue("PXP-OA");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { Complete(false, "AUTOMATION_CONTEXT_FAILED:" + ex.GetType().Name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Queue(string command)
|
||||||
|
{
|
||||||
|
transitions.Add("CommandQueued:" + command);
|
||||||
|
document.SendStringToExecute(command + " ", true, false, false);
|
||||||
|
}
|
||||||
|
private void OnCommandEnded(object sender, CommandEventArgs e)
|
||||||
|
{
|
||||||
|
var name = e.GlobalCommandName?.Trim().TrimStart('.', '_').ToUpperInvariant();
|
||||||
|
if (name is not ("PXP-OA" or "UNDO" or "U")) return;
|
||||||
|
completed.Add(name);
|
||||||
|
transitions.Add("CommandCompleted:" + name);
|
||||||
|
if (step++ == 0) { after1 = Dbmod(); counts1 = Counts(); Queue("PXP-OA"); return; }
|
||||||
|
if (step == 2 && string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { after2 = Dbmod(); counts2 = Counts(); Queue("_.UNDO _1"); return; }
|
||||||
|
if (string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase)) { afterUndo = Dbmod(); countsUndo = Counts(); Complete(true, null); }
|
||||||
|
else { after2 = Dbmod(); counts2 = Counts(); Complete(true, null); }
|
||||||
|
}
|
||||||
|
private void OnCommandFailed(object sender, CommandEventArgs e) => Complete(false, "AUTOMATION_COMMAND_FAILED:" + e.GlobalCommandName);
|
||||||
|
private void OnCommandCancelled(object sender, CommandEventArgs e) => Complete(false, "AUTOMATION_COMMAND_CANCELLED:" + e.GlobalCommandName);
|
||||||
|
|
||||||
|
private int Dbmod() => Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD"));
|
||||||
|
private AutomationEntityCounts Counts()
|
||||||
|
{
|
||||||
|
using var tx = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var table = (BlockTable)tx.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)tx.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
var total = 0; var pipes = 0; var xdata = 0; var owned = 0;
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
total++;
|
||||||
|
if (tx.GetObject(id, OpenMode.ForRead) is BlockReference block && string.Equals(block.Name, "PIPE", StringComparison.OrdinalIgnoreCase)) pipes++;
|
||||||
|
if (tx.GetObject(id, OpenMode.ForRead) is Entity entity)
|
||||||
|
{
|
||||||
|
using var data = entity.GetXDataForApplication("PXP-OA");
|
||||||
|
if (data is not null) { xdata++; if (data.AsArray().Any(x => x.TypeCode == 1000)) owned++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new(total, pipes, xdata, owned);
|
||||||
|
}
|
||||||
|
private FirstApplyAutomationConfiguration ReadConfiguration()
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION"), "FirstLimitedApplyRuntimeTest", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_DISABLED");
|
||||||
|
if (!string.Equals(RuntimeDiagnostics.RuntimeMode, "DryRun", StringComparison.OrdinalIgnoreCase) && !string.Equals(RuntimeDiagnostics.RuntimeMode, "Apply", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AUTOMATION_MODE_BLOCKED");
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION_CONFIG");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_CONFIGURATION_REQUIRED");
|
||||||
|
var loaded = JsonSerializer.Deserialize<FirstApplyAutomationConfiguration>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("AUTOMATION_CONFIGURATION_INVALID");
|
||||||
|
if (!File.Exists(loaded.FixtureMarkerPath) || !string.Equals(File.ReadAllText(loaded.FixtureMarkerPath).Trim(), "P_REC_ANNOT_TEST_FIXTURE=FIRST_LIMITED_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_FIXTURE_MARKER_INVALID");
|
||||||
|
if (!string.Equals(loaded.Phase, "LifecycleProbe", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
(string.IsNullOrWhiteSpace(loaded.OrthoDrawingPath) || !Path.GetFullPath(loaded.OrthoDrawingPath).StartsWith(Path.GetDirectoryName(Path.GetFullPath(loaded.FixtureMarkerPath))! + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
throw new InvalidOperationException("AUTOMATION_FIXTURE_OUTSIDE_MARKER_ROOT");
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
private void Complete(bool success, string? error)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (document is not null) { document.CommandEnded -= OnCommandEnded; document.CommandFailed -= OnCommandFailed; document.CommandCancelled -= OnCommandCancelled; }
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated -= OnDocumentActivated;
|
||||||
|
WriteEvidence(success, error);
|
||||||
|
if (document is not null) document.CloseAndDiscard();
|
||||||
|
AcadApplication.Quit();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally { selectedTargetSnapshotId = null; pxpResults.Clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteEvidence(bool success, string? error)
|
||||||
|
{
|
||||||
|
if (config is null) return;
|
||||||
|
var first = pxpResults.ElementAtOrDefault(0); var second = pxpResults.ElementAtOrDefault(1);
|
||||||
|
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = success, ErrorCode = error, DbmodBefore = before, DbmodAfterCommand1 = after1, DbmodAfterCommand2 = after2, DbmodAfterUndo = afterUndo, Baseline = baseline, AfterCommand1 = counts1, AfterCommand2 = counts2, AfterUndo = countsUndo, CompletedCommands = completed, PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = selectedTargetSnapshotId, ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, PermitAccepted = first?.PermitAccepted, IdleCallbackCount = FirstLimitedApplyAutomationBootstrap.IdleCallbackCount, CoordinatorStartCount = FirstLimitedApplyAutomationBootstrap.CoordinatorStartCount, StateTransitions = transitions };
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||||
|
File.WriteAllText(config.EvidencePath, JsonSerializer.Serialize(evidence));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,11 @@ namespace PRecAnnot.Plant3D2026;
|
|||||||
|
|
||||||
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
||||||
{
|
{
|
||||||
public PlantRuntimeContext ReadCurrentContext()
|
public PlantRuntimeContext ReadCurrentContext() => ReadCurrentContext(null);
|
||||||
|
|
||||||
|
public PlantRuntimeContext ReadCurrentContext(Document? explicitDocument = null)
|
||||||
{
|
{
|
||||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
var document = explicitDocument ?? AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
if (document is null)
|
if (document is null)
|
||||||
{
|
{
|
||||||
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
|
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
|
||||||
|
|||||||
@@ -63,6 +63,24 @@ public sealed class FoundationTests
|
|||||||
Assert.Contains("[CommandMethod(\"PXP-OA\")]", source);
|
Assert.Contains("[CommandMethod(\"PXP-OA\")]", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void AutomationBootstrap_DefersPlantAndDocumentAccessUntilCoordinator()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
var bootstrap = source[..source.IndexOf("internal sealed record FirstApplyAutomationEvidence", StringComparison.Ordinal)];
|
||||||
|
Assert.DoesNotContain("PlantProject.LoadProject", bootstrap);
|
||||||
|
Assert.DoesNotContain("MdiActiveDocument", bootstrap);
|
||||||
|
Assert.Contains("AcadApplication.Idle += OnIdle", bootstrap);
|
||||||
|
Assert.Contains("coordinatorStartCount++", bootstrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AutomationLifecycleProbe_CompletesBeforePlantActivation()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "FirstLimitedApplyAutomation.cs"));
|
||||||
|
Assert.Contains("LifecycleProbe", source);
|
||||||
|
Assert.True(source.IndexOf("CompleteLifecycleProbe", StringComparison.Ordinal) < source.IndexOf("PlantProject.LoadProject", StringComparison.Ordinal));
|
||||||
|
Assert.Contains("PostStartupReady", source);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
|
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
|
||||||
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
|
|||||||
Reference in New Issue
Block a user