feature: In-App Simulation Runner (Backend)
This commit is contained in:
@@ -3,6 +3,7 @@ using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
using VigilCare.Simulation;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class ClinicalRefactorEndToEndTests : IAsyncLifetime
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StackExchange.Redis;
|
||||
|
||||
@@ -24,6 +26,12 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
public static int RabbitPort { get; } =
|
||||
int.TryParse(Environment.GetEnvironmentVariable("RabbitMq__Port"), out var p) ? p : 5674;
|
||||
|
||||
public static string SimulationScenarioDirectory { get; } = Path.GetFullPath(Path.Combine(
|
||||
AppContext.BaseDirectory, "Fixtures", "Scenarios"));
|
||||
|
||||
public TestSimulationClientFactory SimulationClientFactory =>
|
||||
Services.GetRequiredService<TestSimulationClientFactory>();
|
||||
|
||||
// Override configuration to point at a test database — never run tests against
|
||||
// the development database; a botched rollback could corrupt seed data.
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@@ -62,6 +70,16 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
});
|
||||
|
||||
config.AddJsonFile("appsettings.Testing.json", optional: true, reloadOnChange: false);
|
||||
|
||||
// Win over appsettings.Testing.json catalogue path / concurrency defaults.
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Simulation:Enabled"] = "true",
|
||||
["Simulation:ScenarioDirectory"] = SimulationScenarioDirectory,
|
||||
["Simulation:MaxConcurrentRuns"] = "2",
|
||||
["Simulation:MaxSpeed"] = "600",
|
||||
["Simulation:RunHistoryLimit"] = "50",
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
@@ -77,6 +95,15 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
.AddScheme<AuthenticationSchemeOptions, TestingAuthHandler>(
|
||||
TestingAuthHandler.SchemeName, _ => { });
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<ISimulationClientFactory>();
|
||||
services.AddSingleton<TestSimulationClientFactory>(_ =>
|
||||
new TestSimulationClientFactory(this));
|
||||
services.AddSingleton<ISimulationClientFactory>(sp =>
|
||||
sp.GetRequiredService<TestSimulationClientFactory>());
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"scenario": {
|
||||
"id": "minimal-sim-01",
|
||||
"name": "Minimal Simulation Fixture",
|
||||
"description": "Three observation clusters for Phase 36 CI tests.",
|
||||
"durationMinutes": 2,
|
||||
"tags": ["test", "minimal"]
|
||||
},
|
||||
"patient": {
|
||||
"firstName": "Sim",
|
||||
"lastName": "Fixture",
|
||||
"dateOfBirth": "1980-01-15",
|
||||
"gender": "Female"
|
||||
},
|
||||
"encounter": {
|
||||
"department": "GeneralMedicine",
|
||||
"encounterType": "Inpatient",
|
||||
"attendingPhysician": "Dr. Test",
|
||||
"roomBed": "T-1",
|
||||
"admissionReason": "Simulation fixture"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"offsetMinutes": 0,
|
||||
"type": "observation",
|
||||
"data": { "code": "HEART_RATE", "value": 72, "unit": "bpm", "source": "Manual" }
|
||||
},
|
||||
{
|
||||
"offsetMinutes": 0,
|
||||
"type": "observation",
|
||||
"data": { "code": "RESP_RATE", "value": 14, "unit": "/min", "source": "Manual" }
|
||||
},
|
||||
{
|
||||
"offsetMinutes": 1,
|
||||
"type": "observation",
|
||||
"data": { "code": "HEART_RATE", "value": 74, "unit": "bpm", "source": "Manual" }
|
||||
},
|
||||
{
|
||||
"offsetMinutes": 2,
|
||||
"type": "observation",
|
||||
"data": { "code": "HEART_RATE", "value": 70, "unit": "bpm", "source": "Manual" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public static class DbResetHelper
|
||||
DELETE FROM alert_thresholds;
|
||||
DELETE FROM clinical_audit_logs;
|
||||
DELETE FROM clinical_users;
|
||||
DELETE FROM simulation_runs;
|
||||
DELETE FROM patients;
|
||||
");
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using VigilCare.Simulation;
|
||||
|
||||
public static class ScenarioReplayHelper
|
||||
{
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class SimulationEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public SimulationEndpointTests(ApiFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_client = fixture.CreateClient();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
||||
await DbResetHelper.ResetAsync(db);
|
||||
await DataSeeder.SeedThresholdsOnlyAsync(db, redis);
|
||||
|
||||
_fixture.SimulationClientFactory.HangOnCreate = false;
|
||||
_fixture.SimulationClientFactory.FailOnCreate = false;
|
||||
|
||||
_client.DefaultRequestHeaders.Remove("X-Test-Role");
|
||||
_client.DefaultRequestHeaders.Remove("X-Test-User-Id");
|
||||
_client.AsAdmin();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Config_ReturnsEnabledFalse_WhenDisabled()
|
||||
{
|
||||
using var factory = _fixture.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Simulation:Enabled"] = "false",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var client = factory.CreateClient();
|
||||
client.AsAdmin();
|
||||
|
||||
var resp = await client.GetAsync("/api/v1/simulation/config");
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
body.GetProperty("data").GetProperty("enabled").GetBoolean().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_WhenDisabled_Returns404()
|
||||
{
|
||||
using var factory = _fixture.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Simulation:Enabled"] = "false",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var client = factory.CreateClient();
|
||||
client.AsAdmin();
|
||||
|
||||
var resp = await client.GetAsync("/api/v1/simulation/scenarios");
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_AsNurse_Returns200()
|
||||
{
|
||||
_client.AsNurse();
|
||||
|
||||
var resp = await _client.GetAsync("/api/v1/simulation/scenarios");
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var items = body.GetProperty("data");
|
||||
items.GetArrayLength().Should().BeGreaterThan(0);
|
||||
items[0].GetProperty("id").GetString().Should().Be("minimal-sim-01");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartRun_AsIntegrationRole_Returns403()
|
||||
{
|
||||
_client.AsIntegration();
|
||||
|
||||
var resp = await _client.PostAsJsonAsync("/api/v1/simulation/runs", new
|
||||
{
|
||||
scenarioId = "minimal-sim-01",
|
||||
speed = 600
|
||||
});
|
||||
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartRun_WritesAuditLog()
|
||||
{
|
||||
_client.AsPhysician();
|
||||
|
||||
var resp = await _client.PostAsJsonAsync("/api/v1/simulation/runs", new
|
||||
{
|
||||
scenarioId = "minimal-sim-01",
|
||||
speed = 600
|
||||
});
|
||||
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var runId = body.GetProperty("data").GetProperty("runId").GetGuid();
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var audit = await db.ClinicalAuditLogs
|
||||
.Where(a => a.Action == AuditAction.SimulationRunStarted && a.EntityId == runId)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
audit.Should().NotBeNull();
|
||||
audit!.EntityType.Should().Be("SimulationRun");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Config_WhenEnabled_ReturnsLimits()
|
||||
{
|
||||
_client.AsNurse();
|
||||
|
||||
var resp = await _client.GetAsync("/api/v1/simulation/config");
|
||||
resp.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var data = body.GetProperty("data");
|
||||
data.GetProperty("enabled").GetBoolean().Should().BeTrue();
|
||||
data.GetProperty("maxSpeed").GetDouble().Should().Be(600);
|
||||
data.GetProperty("maxConcurrentRuns").GetInt32().Should().Be(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StackExchange.Redis;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class SimulationRunnerTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
private ISimulationRunner _runner = null!;
|
||||
private TestSimulationClientFactory _clientFactory = null!;
|
||||
|
||||
public SimulationRunnerTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
||||
await DbResetHelper.ResetAsync(db);
|
||||
await DataSeeder.SeedThresholdsOnlyAsync(db, redis);
|
||||
|
||||
_runner = _fixture.Services.GetRequiredService<ISimulationRunner>();
|
||||
_clientFactory = _fixture.SimulationClientFactory;
|
||||
_clientFactory.HangOnCreate = false;
|
||||
_clientFactory.FailOnCreate = false;
|
||||
|
||||
foreach (var run in _runner.ListRuns())
|
||||
_runner.Cancel(run.RunId);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_clientFactory.HangOnCreate = false;
|
||||
_clientFactory.FailOnCreate = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_UnknownScenario_Returns422()
|
||||
{
|
||||
var act = () => _runner.StartAsync("does-not-exist", 60, "tester", CancellationToken.None);
|
||||
|
||||
var ex = await act.Should().ThrowAsync<ValidationException>();
|
||||
ex.Which.ErrorCode.Should().Be("SIMULATION_SCENARIO_UNKNOWN");
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
(await db.SimulationRuns.CountAsync()).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_SpeedAboveMax_Returns422()
|
||||
{
|
||||
var act = () => _runner.StartAsync("minimal-sim-01", 601, "tester", CancellationToken.None);
|
||||
|
||||
var ex = await act.Should().ThrowAsync<ValidationException>();
|
||||
ex.Which.ErrorCode.Should().Be("SIMULATION_SPEED_INVALID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_AtConcurrencyLimit_Returns409()
|
||||
{
|
||||
_clientFactory.HangOnCreate = true;
|
||||
|
||||
await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
||||
await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
||||
|
||||
var act = () => _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
||||
var ex = await act.Should().ThrowAsync<ConflictException>();
|
||||
ex.Which.ErrorCode.Should().Be("SIMULATION_CONCURRENCY_LIMIT");
|
||||
|
||||
foreach (var run in _runner.ListRuns())
|
||||
_runner.Cancel(run.RunId);
|
||||
|
||||
await WaitForAsync(() => _runner.ListRuns().All(r =>
|
||||
r.Status is SimulationRunStatus.Cancelled or SimulationRunStatus.Failed
|
||||
or SimulationRunStatus.Completed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Start_CreatesPatientMarkedSimulated()
|
||||
{
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
||||
var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
||||
|
||||
completed.PatientId.Should().NotBeNull();
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var patient = await db.Patients.SingleAsync(p => p.Id == completed.PatientId);
|
||||
patient.IsSimulated.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_ProgressAdvances()
|
||||
{
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
||||
|
||||
await WaitForAsync(() =>
|
||||
{
|
||||
var current = _runner.GetRun(state.RunId);
|
||||
return current is not null && current.LastOffsetMinutes > 0;
|
||||
});
|
||||
|
||||
var mid = _runner.GetRun(state.RunId)!;
|
||||
mid.LastOffsetMinutes.Should().BeGreaterThan(0);
|
||||
|
||||
var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
||||
completed.ProgressPercent.Should().Be(100);
|
||||
completed.ObservationsSent.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_CancelsRun_StatusCancelled()
|
||||
{
|
||||
_clientFactory.HangOnCreate = true;
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
||||
|
||||
_runner.Cancel(state.RunId).Should().BeTrue();
|
||||
var cancelled = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
|
||||
cancelled.Status.Should().Be(SimulationRunStatus.Cancelled);
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
(await db.Observations.CountAsync()).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_AlreadyCompleted_IsNoOpSuccess()
|
||||
{
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
||||
await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
|
||||
|
||||
_runner.Cancel(state.RunId).Should().BeTrue();
|
||||
var after = _runner.GetRun(state.RunId)!;
|
||||
after.Status.Should().Be(SimulationRunStatus.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Failure_RecordsFailureReason()
|
||||
{
|
||||
_clientFactory.FailOnCreate = true;
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
|
||||
|
||||
var failed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Failed);
|
||||
failed.FailureReason.Should().NotBeNullOrWhiteSpace();
|
||||
failed.FailureReason.Should().Contain("Login failed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Shutdown_CancelsActiveRuns()
|
||||
{
|
||||
_clientFactory.HangOnCreate = true;
|
||||
var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
|
||||
|
||||
var hosted = (IHostedService)_fixture.Services.GetRequiredService<SimulationRunner>();
|
||||
await hosted.StopAsync(CancellationToken.None);
|
||||
|
||||
var after = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
|
||||
after.Status.Should().Be(SimulationRunStatus.Cancelled);
|
||||
}
|
||||
|
||||
private async Task<SimulationRunState> WaitForRunAsync(
|
||||
Guid runId, SimulationRunStatus expected, TimeSpan? timeout = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WaitForAsync(() => _runner.GetRun(runId)?.Status == expected, timeout);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
var actual = _runner.GetRun(runId);
|
||||
throw new TimeoutException(
|
||||
$"Expected run {runId} status {expected}, but was {actual?.Status}. " +
|
||||
$"FailureReason={actual?.FailureReason}");
|
||||
}
|
||||
|
||||
return _runner.GetRun(runId)!;
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> condition, TimeSpan? timeout = null)
|
||||
{
|
||||
var deadline = DateTimeOffset.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
if (condition())
|
||||
return;
|
||||
await Task.Delay(25);
|
||||
}
|
||||
|
||||
throw new TimeoutException("Condition was not met within the timeout.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using VigilCare.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Loopback client for WebApplicationFactory tests. Uses X-Test-Role INTEGRATION
|
||||
/// instead of JWT login (Testing auth scheme ignores Bearer tokens).
|
||||
/// Uses Server.CreateHandler() to avoid TestServer re-entrancy deadlocks.
|
||||
/// </summary>
|
||||
public sealed class TestSimulationClientFactory : ISimulationClientFactory
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public TestSimulationClientFactory(WebApplicationFactory<Program> factory) =>
|
||||
_factory = factory;
|
||||
|
||||
/// <summary>When true, CreateAsync blocks until cancelled — for concurrency/shutdown tests.</summary>
|
||||
public bool HangOnCreate { get; set; }
|
||||
|
||||
/// <summary>When true, CreateAsync throws — for failure-path tests.</summary>
|
||||
public bool FailOnCreate { get; set; }
|
||||
|
||||
public async Task<VigilCareApiClient> CreateAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (FailOnCreate)
|
||||
throw new HttpRequestException("Login failed (401 Unauthorized): invalid credentials");
|
||||
|
||||
if (HangOnCreate)
|
||||
await Task.Delay(Timeout.Infinite, ct);
|
||||
|
||||
var http = new HttpClient(_factory.Server.CreateHandler(), disposeHandler: true)
|
||||
{
|
||||
BaseAddress = _factory.Server.BaseAddress ?? new Uri("http://localhost"),
|
||||
};
|
||||
http.DefaultRequestHeaders.Add("X-Test-Role", "INTEGRATION");
|
||||
return new VigilCareApiClient(http);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VigilCareClinicalAPI\VigilCareClinicalAPI.csproj" />
|
||||
<ProjectReference Include="..\VigilCare.Simulation.Core\VigilCare.Simulation.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\VigilCare.Simulator\Scenarios\ScenarioFile.cs" Link="Scenarios\ScenarioFile.cs" />
|
||||
<Compile Include="..\VigilCare.Simulator\Scenarios\ScenarioValidator.cs" Link="Scenarios\ScenarioValidator.cs" />
|
||||
<None Include="Fixtures\Scenarios\**\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user