diff --git a/BugPro/BugPro.csproj b/BugPro/BugPro.csproj
new file mode 100644
index 0000000..92333c8
--- /dev/null
+++ b/BugPro/BugPro.csproj
@@ -0,0 +1,15 @@
+
+
+ Exe
+ net9.0
+ BugPro
+ BugPro
+ enable
+ enable
+ 13
+ false
+
+
+
+
+
diff --git a/BugPro/Program.cs b/BugPro/Program.cs
new file mode 100644
index 0000000..19550c7
--- /dev/null
+++ b/BugPro/Program.cs
@@ -0,0 +1,152 @@
+using Stateless;
+
+namespace BugPro;
+
+public enum IssuePhase
+{
+ New,
+ Analysis,
+ InProgress,
+ NeedMoreInfo,
+ NotReproducible,
+ Duplicate,
+ Fixed,
+ Closed,
+ Reopened,
+ Deferred
+}
+
+public enum TransitionSignal
+{
+ StartAnalysis,
+ RequestInfo,
+ MarkDuplicate,
+ MarkNotReproducible,
+ StartFix,
+ Fix,
+ ConfirmResolved,
+ RejectFix,
+ Reopen,
+ Defer,
+ ReturnToAnalysis
+}
+
+public sealed class Bug
+{
+ private IssuePhase phase;
+ private readonly StateMachine engine;
+
+ public Bug()
+ {
+ phase = IssuePhase.New;
+ engine = new StateMachine(
+ () => phase,
+ s => phase = s);
+
+ BuildWorkflow();
+ }
+
+ public IssuePhase State => phase;
+
+ public void StartAnalysis() => Apply(TransitionSignal.StartAnalysis);
+
+ public void RequestInfo() => Apply(TransitionSignal.RequestInfo);
+
+ public void MarkDuplicate() => Apply(TransitionSignal.MarkDuplicate);
+
+ public void MarkNotReproducible() => Apply(TransitionSignal.MarkNotReproducible);
+
+ public void StartFix() => Apply(TransitionSignal.StartFix);
+
+ public void Fix() => Apply(TransitionSignal.Fix);
+
+ public void ConfirmResolved() => Apply(TransitionSignal.ConfirmResolved);
+
+ public void RejectFix() => Apply(TransitionSignal.RejectFix);
+
+ public void Reopen() => Apply(TransitionSignal.Reopen);
+
+ public void Defer() => Apply(TransitionSignal.Defer);
+
+ public void ReturnToAnalysis() => Apply(TransitionSignal.ReturnToAnalysis);
+
+ public bool CanStartAnalysis() => engine.CanFire(TransitionSignal.StartAnalysis);
+
+ public bool CanFix() => engine.CanFire(TransitionSignal.Fix);
+
+ public bool CanClose() => engine.CanFire(TransitionSignal.ConfirmResolved);
+
+ public bool CanReopen() => engine.CanFire(TransitionSignal.Reopen);
+
+ public bool CanReturnToAnalysis() => engine.CanFire(TransitionSignal.ReturnToAnalysis);
+
+ private void Apply(TransitionSignal signal) => engine.Fire(signal);
+
+ private void BuildWorkflow()
+ {
+ engine.Configure(IssuePhase.New)
+ .Permit(TransitionSignal.StartAnalysis, IssuePhase.Analysis)
+ .Permit(TransitionSignal.Defer, IssuePhase.Deferred);
+
+ engine.Configure(IssuePhase.Analysis)
+ .Permit(TransitionSignal.RequestInfo, IssuePhase.NeedMoreInfo)
+ .Permit(TransitionSignal.MarkDuplicate, IssuePhase.Duplicate)
+ .Permit(TransitionSignal.MarkNotReproducible, IssuePhase.NotReproducible)
+ .Permit(TransitionSignal.StartFix, IssuePhase.InProgress)
+ .Permit(TransitionSignal.Defer, IssuePhase.Deferred);
+
+ engine.Configure(IssuePhase.NeedMoreInfo)
+ .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis)
+ .Permit(TransitionSignal.Defer, IssuePhase.Deferred);
+
+ engine.Configure(IssuePhase.InProgress)
+ .Permit(TransitionSignal.Fix, IssuePhase.Fixed)
+ .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis)
+ .Permit(TransitionSignal.Defer, IssuePhase.Deferred);
+
+ engine.Configure(IssuePhase.Fixed)
+ .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed)
+ .Permit(TransitionSignal.RejectFix, IssuePhase.Reopened);
+
+ engine.Configure(IssuePhase.Closed)
+ .Permit(TransitionSignal.Reopen, IssuePhase.Reopened);
+
+ engine.Configure(IssuePhase.Reopened)
+ .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis)
+ .Permit(TransitionSignal.StartFix, IssuePhase.InProgress)
+ .Permit(TransitionSignal.Defer, IssuePhase.Deferred);
+
+ engine.Configure(IssuePhase.Deferred)
+ .Permit(TransitionSignal.ReturnToAnalysis, IssuePhase.Analysis);
+
+ engine.Configure(IssuePhase.Duplicate)
+ .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed);
+
+ engine.Configure(IssuePhase.NotReproducible)
+ .Permit(TransitionSignal.ConfirmResolved, IssuePhase.Closed);
+ }
+}
+
+internal static class Program
+{
+ private static void Main()
+ {
+ var ticket = new Bug();
+
+ PrintPhase(ticket, "регистрация");
+ ticket.Defer();
+ PrintPhase(ticket, "отложен");
+ ticket.ReturnToAnalysis();
+ ticket.StartAnalysis();
+ PrintPhase(ticket, "разбор");
+ ticket.StartFix();
+ ticket.Fix();
+ ticket.ConfirmResolved();
+ PrintPhase(ticket, "завершён");
+ }
+
+ private static void PrintPhase(Bug ticket, string step)
+ {
+ Console.WriteLine($"[{step}] фаза: {ticket.State}");
+ }
+}
diff --git a/BugTests/BugTests.csproj b/BugTests/BugTests.csproj
new file mode 100644
index 0000000..7e8017c
--- /dev/null
+++ b/BugTests/BugTests.csproj
@@ -0,0 +1,21 @@
+
+
+ net9.0
+ BugTests
+ BugTests
+ enable
+ enable
+ 13
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/BugTests/UnitTest1.cs b/BugTests/UnitTest1.cs
new file mode 100644
index 0000000..ea0b56f
--- /dev/null
+++ b/BugTests/UnitTest1.cs
@@ -0,0 +1,240 @@
+using BugPro;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace BugTests;
+
+[TestClass]
+public sealed class IssueWorkflowTests
+{
+ private Bug _ticket = null!;
+
+ [TestInitialize]
+ public void Init() => _ticket = new Bug();
+
+ [TestCleanup]
+ public void Cleanup() => _ticket = null!;
+
+ [TestMethod]
+ public void FreshTicket_StartsInNewPhase()
+ {
+ ExpectPhase(IssuePhase.New);
+ }
+
+ [TestMethod]
+ public void NewTicket_OnlyStartAnalysisAllowed()
+ {
+ Assert.IsTrue(_ticket.CanStartAnalysis());
+ Assert.IsFalse(_ticket.CanFix());
+ Assert.IsFalse(_ticket.CanClose());
+ Assert.IsFalse(_ticket.CanReopen());
+ Assert.IsFalse(_ticket.CanReturnToAnalysis());
+ }
+
+ [TestMethod]
+ public void BeginReview_MovesToAnalysisPhase()
+ {
+ _ticket.StartAnalysis();
+ ExpectPhase(IssuePhase.Analysis);
+ }
+
+ [TestMethod]
+ public void RequestDetails_FromAnalysis_GoesToNeedMoreInfo()
+ {
+ GoToAnalysis();
+ _ticket.RequestInfo();
+ ExpectPhase(IssuePhase.NeedMoreInfo);
+ }
+
+ [TestMethod]
+ public void ReturnFromDetails_RestoresAnalysis()
+ {
+ GoToNeedMoreInfo();
+ _ticket.ReturnToAnalysis();
+ ExpectPhase(IssuePhase.Analysis);
+ }
+
+ [TestMethod]
+ public void LaunchFix_FromAnalysis_EntersInProgress()
+ {
+ GoToAnalysis();
+ _ticket.StartFix();
+ ExpectPhase(IssuePhase.InProgress);
+ }
+
+ [TestMethod]
+ public void InProgress_AllowsFixAction()
+ {
+ GoToInProgress();
+ Assert.IsTrue(_ticket.CanFix());
+ Assert.IsFalse(_ticket.CanStartAnalysis());
+ }
+
+ [TestMethod]
+ public void ApplyFix_ReachesResolvedPhase()
+ {
+ GoToInProgress();
+ _ticket.Fix();
+ ExpectPhase(IssuePhase.Fixed);
+ }
+
+ [TestMethod]
+ public void CloseTicket_CompletesHappyPath()
+ {
+ GoToInProgress();
+ _ticket.Fix();
+ _ticket.ConfirmResolved();
+ ExpectPhase(IssuePhase.Closed);
+ }
+
+ [TestMethod]
+ public void Reopen_FromClosed_SetsReopenedPhase()
+ {
+ GoToClosed();
+ _ticket.Reopen();
+ ExpectPhase(IssuePhase.Reopened);
+ }
+
+ [TestMethod]
+ public void RejectResolution_FromFixed_OpensReopened()
+ {
+ GoToFixed();
+ _ticket.RejectFix();
+ ExpectPhase(IssuePhase.Reopened);
+ }
+
+ [TestMethod]
+ public void Reopened_ResumesFixWork()
+ {
+ GoToReopened();
+ _ticket.StartFix();
+ ExpectPhase(IssuePhase.InProgress);
+ }
+
+ [TestMethod]
+ public void MarkAsDuplicate_AndClose()
+ {
+ GoToAnalysis();
+ _ticket.MarkDuplicate();
+ _ticket.ConfirmResolved();
+ ExpectPhase(IssuePhase.Closed);
+ }
+
+ [TestMethod]
+ public void MarkAsNonReproducible_AndClose()
+ {
+ GoToAnalysis();
+ _ticket.MarkNotReproducible();
+ _ticket.ConfirmResolved();
+ ExpectPhase(IssuePhase.Closed);
+ }
+
+ [TestMethod]
+ public void Postpone_FromNew_GoesDeferred()
+ {
+ _ticket.Defer();
+ ExpectPhase(IssuePhase.Deferred);
+ }
+
+ [TestMethod]
+ public void Deferred_ReturnsToAnalysis()
+ {
+ _ticket.Defer();
+ _ticket.ReturnToAnalysis();
+ ExpectPhase(IssuePhase.Analysis);
+ }
+
+ [TestMethod]
+ public void Fix_FromNew_Throws()
+ {
+ AssertBlocked(() => _ticket.Fix());
+ }
+
+ [TestMethod]
+ public void Close_FromAnalysis_Throws()
+ {
+ GoToAnalysis();
+ AssertBlocked(() => _ticket.ConfirmResolved());
+ }
+
+ [TestMethod]
+ public void Reopen_FromFixed_Throws()
+ {
+ GoToFixed();
+ AssertBlocked(() => _ticket.Reopen());
+ }
+
+ [TestMethod]
+ public void StartAnalysis_FromClosed_Throws()
+ {
+ GoToClosed();
+ AssertBlocked(() => _ticket.StartAnalysis());
+ }
+
+ [TestMethod]
+ public void ReturnToAnalysis_FromNew_Throws()
+ {
+ AssertBlocked(() => _ticket.ReturnToAnalysis());
+ }
+
+ [TestMethod]
+ public void Defer_FromClosed_Throws()
+ {
+ GoToClosed();
+ AssertBlocked(() => _ticket.Defer());
+ }
+
+ [TestMethod]
+ public void MarkDuplicate_FromNew_Throws()
+ {
+ AssertBlocked(() => _ticket.MarkDuplicate());
+ }
+
+ [TestMethod]
+ public void RequestInfo_FromInProgress_Throws()
+ {
+ GoToInProgress();
+ AssertBlocked(() => _ticket.RequestInfo());
+ }
+
+ private void ExpectPhase(IssuePhase expected)
+ {
+ Assert.AreEqual(expected, _ticket.State);
+ }
+
+ private static void AssertBlocked(Action action)
+ {
+ Assert.ThrowsException(action);
+ }
+
+ private void GoToAnalysis() => _ticket.StartAnalysis();
+
+ private void GoToNeedMoreInfo()
+ {
+ GoToAnalysis();
+ _ticket.RequestInfo();
+ }
+
+ private void GoToInProgress()
+ {
+ GoToAnalysis();
+ _ticket.StartFix();
+ }
+
+ private void GoToFixed()
+ {
+ GoToInProgress();
+ _ticket.Fix();
+ }
+
+ private void GoToClosed()
+ {
+ GoToFixed();
+ _ticket.ConfirmResolved();
+ }
+
+ private void GoToReopened()
+ {
+ GoToFixed();
+ _ticket.RejectFix();
+ }
+}
diff --git a/ST-4.sln b/ST-4.sln
index 58ea566..a4943af 100644
--- a/ST-4.sln
+++ b/ST-4.sln
@@ -1,14 +1,27 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugPro", "BugPro\BugPro.csproj", "{B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugTests", "BugTests\BugTests.csproj", "{262BB970-40D8-4B89-A423-615CE07FC740}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B97A1CE8-1D2F-4B57-915F-BCA06F1D4833}.Release|Any CPU.Build.0 = Release|Any CPU
+ {262BB970-40D8-4B89-A423-615CE07FC740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {262BB970-40D8-4B89-A423-615CE07FC740}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {262BB970-40D8-4B89-A423-615CE07FC740}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {262BB970-40D8-4B89-A423-615CE07FC740}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
-EndGlobal
+EndGlobal
\ No newline at end of file