using Microsoft.Data.Sqlite; using OpenNest.Data; namespace OpenNest.Server.Tests; /// /// My Server Jobs on the database: list filters and queue positions, Keep and Discard each /// happening once per result, removal, the worker's summary and configurable retention. /// public class NestJobListTests { private static readonly DateTimeOffset T0 = new(2026, 11, 2, 8, 0, 0, TimeSpan.Zero); private static readonly byte[] Package = { 1, 2, 3 }; private static Guid Submit(NestDatabase db, string title, string by = "aj@pc1", DateTimeOffset? at = null, Guid? nestId = null) { var id = Guid.NewGuid(); db.InsertJob(id, new NestJobSubmission { Title = title, EngineName = "Default", SubmittedBy = by, NestId = nestId }, Package, at ?? T0); return id; } // Claims the oldest queued job and completes it. private static Guid Finish(NestDatabase db, DateTimeOffset at, NestJobSummary? summary = null) { var job = db.ClaimJob("w1", at)!; Assert.True(db.CompleteJob(job.Id, "w1", "{}", at.AddMinutes(1), summary)); return job.Id; } private static (ServerFactory Factory, NestDatabase Db) Open() { var factory = new ServerFactory(); return (factory, new NestDatabase(factory.DatabasePath, pooling: false)); } private static IEnumerable Titles(IEnumerable jobs) => jobs.Select(j => j.Title); [Fact] public void TheListFiltersByStateAndSubmitter() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "done", "AJ@pc1"); Finish(db, T0); Submit(db, "running", "aj@pc2"); db.ClaimJob("w1", T0.AddMinutes(2)); Submit(db, "queued", "bob@pc3"); Submit(db, "plain", "aj"); Submit(db, "lookalike", "ajax@pc4"); var now = T0.AddMinutes(3); Assert.Equal(new[] { "lookalike", "plain", "queued", "running", "done" }, Titles(db.ListJobs(NestJobListFilter.All, null, 100, now))); Assert.Equal(new[] { "lookalike", "plain", "queued", "running" }, Titles(db.ListJobs(NestJobListFilter.Active, null, 100, now))); Assert.Equal(new[] { "done" }, Titles(db.ListJobs(NestJobListFilter.Finished, null, 100, now))); Assert.Equal(new[] { "done" }, Titles(db.ListJobs(NestJobListFilter.Unreviewed, null, 100, now))); // A bare user name is that user on any workstation, ignoring case; a full one is one workstation. Assert.Equal(new[] { "plain", "running", "done" }, Titles(db.ListJobs(NestJobListFilter.All, "aj", 100, now))); Assert.Equal(new[] { "running" }, Titles(db.ListJobs(NestJobListFilter.All, "AJ@PC2", 100, now))); Assert.Equal(new[] { "running" }, Titles(db.ListJobs(NestJobListFilter.Active, "aj@pc2", 100, now))); Assert.Equal(new[] { "lookalike", "plain" }, Titles(db.ListJobs(NestJobListFilter.All, null, 2, now))); } } [Fact] public void ListedQueuedJobsShowTheirPlaceInTheQueue() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "a"); Submit(db, "b"); Submit(db, "c"); db.ClaimJob("w1", T0); var jobs = db.ListJobs(NestJobListFilter.All, null, 100, T0).ToDictionary(j => j.Title); Assert.Equal(NestJobState.Running, jobs["a"].State); Assert.Equal(0, jobs["a"].QueuePosition); Assert.Equal(0, jobs["b"].QueuePosition); Assert.Equal(1, jobs["c"].QueuePosition); Assert.Equal(1, db.GetJob(jobs["c"].Id)!.QueuePosition); db.CancelJob(jobs["b"].Id, T0); Assert.Equal(0, db.ListJobs(NestJobListFilter.Active, null, 100, T0).Single(j => j.Title == "c").QueuePosition); } } [Fact] public void FinishedJobsCarryTheWorkersSummaryAndTheirNest() { var (factory, db) = Open(); using (factory) using (db) { var nest = Guid.NewGuid(); Submit(db, "with", nestId: nest); var with = Finish(db, T0, new NestJobSummary { PartsPlaced = 23, PartsUnplaced = 2, Sheets = 3, Utilization = 0.784 }); Submit(db, "without"); var without = Finish(db, T0.AddMinutes(2)); var job = db.GetJob(with)!; Assert.Equal(nest, job.NestId); Assert.Equal(23, job.Summary!.PartsPlaced); Assert.Equal(2, job.Summary.PartsUnplaced); Assert.Equal(3, job.Summary.Sheets); Assert.Equal(0.784, job.Summary.Utilization, 6); Assert.True(job.IsUnreviewed); Assert.Null(db.GetJob(without)!.Summary); Assert.Null(db.GetJob(without)!.NestId); } } [Fact] public void AResultIsKeptOnlyOnce() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "n"); var id = Finish(db, T0); var seen = db.GetJob(id)!; // Two desktops reviewed the same revision; the first Keep wins. var first = db.ReviewJob(id, NestJobReview.Kept, "aj@pc1", seen.Revision, T0.AddMinutes(5))!.Value; Assert.True(first.Changed); Assert.Equal(NestJobReview.Kept, first.Job.Review); Assert.Equal("aj@pc1", first.Job.ReviewedBy); Assert.Equal(T0.AddMinutes(5).UtcDateTime, first.Job.ReviewedAt); Assert.False(first.Job.IsUnreviewed); Assert.True(first.Job.Revision > seen.Revision); var second = db.ReviewJob(id, NestJobReview.Kept, "bob@pc2", seen.Revision, T0.AddMinutes(6))!.Value; Assert.False(second.Changed); Assert.Equal("aj@pc1", second.Job.ReviewedBy); // Even with the new revision a kept result cannot be kept or discarded again. Assert.False(db.ReviewJob(id, NestJobReview.Kept, "bob@pc2", first.Job.Revision, T0)!.Value.Changed); Assert.False(db.ReviewJob(id, NestJobReview.Discarded, "bob@pc2", first.Job.Revision, T0)!.Value.Changed); Assert.Empty(db.ListJobs(NestJobListFilter.Unreviewed, null, 100, T0)); Assert.Null(db.ReviewJob(Guid.NewGuid(), NestJobReview.Kept, "", 1, T0)); } } [Fact] public void ConcurrentKeepsLetOneDesktopKeep() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "n"); var id = Finish(db, T0); var revision = db.GetJob(id)!.Revision; var kept = Enumerable.Range(0, 16).AsParallel() .Select(i => db.ReviewJob(id, NestJobReview.Kept, $"pc{i}", revision, T0)!.Value.Changed) .Count(changed => changed); Assert.Equal(1, kept); } } [Fact] public void ADiscardedResultCannotBeKeptAndOnlySucceededJobsAreReviewed() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "n"); var id = Finish(db, T0); var revision = db.GetJob(id)!.Revision; Assert.True(db.ReviewJob(id, NestJobReview.Discarded, "aj@pc1", revision, T0)!.Value.Changed); var discarded = db.GetJob(id)!; Assert.Equal(NestJobReview.Discarded, discarded.Review); Assert.False(db.ReviewJob(id, NestJobReview.Kept, "aj@pc1", discarded.Revision, T0)!.Value.Changed); var queued = Submit(db, "q"); Assert.False(db.ReviewJob(queued, NestJobReview.Kept, "aj@pc1", db.GetJob(queued)!.Revision, T0)!.Value.Changed); Assert.Throws(() => db.ReviewJob(queued, NestJobReview.None, "", 1, T0)); } } [Fact] public void OnlyFinishedJobsAtTheirRevisionAreRemoved() { var (factory, db) = Open(); using (factory) using (db) { Submit(db, "n"); var done = Finish(db, T0); var queued = Submit(db, "q"); var stale = db.DeleteJob(done, db.GetJob(done)!.Revision - 1)!.Value; Assert.False(stale.Removed); Assert.NotNull(stale.Job); Assert.False(db.DeleteJob(queued, db.GetJob(queued)!.Revision)!.Value.Removed); Assert.True(db.DeleteJob(done, db.GetJob(done)!.Revision)!.Value.Removed); Assert.Null(db.GetJob(done)); Assert.Null(db.DeleteJob(done, 1)); } } [Fact] public void RetentionIsConfigurableAndAppliedWhenListing() { var (factory, db) = Open(); using (factory) using (db) { db.JobRetention = TimeSpan.FromDays(2); Submit(db, "old"); var old = Finish(db, T0); Submit(db, "recent", at: T0.AddDays(1)); var recent = Finish(db, T0.AddDays(1)); Submit(db, "waiting"); var jobs = db.ListJobs(NestJobListFilter.All, null, 100, T0.AddDays(2).AddMinutes(2)); Assert.Equal(new[] { "waiting", "recent" }, Titles(jobs)); Assert.Null(db.GetJob(old)); Assert.NotNull(db.GetJob(recent)); } } [Fact] public void RetentionReadsDaysFromTheEnvironment() { Assert.Equal(NestJobRules.Retention, ServerOptions.JobRetention(null)); Assert.Equal(NestJobRules.Retention, ServerOptions.JobRetention(" ")); Assert.Equal(TimeSpan.FromDays(30), ServerOptions.JobRetention("30")); Assert.Equal(TimeSpan.FromHours(12), ServerOptions.JobRetention("0.5")); Assert.Throws(() => ServerOptions.JobRetention("0")); Assert.Throws(() => ServerOptions.JobRetention("-3")); Assert.Throws(() => ServerOptions.JobRetention("week")); } [Fact] public void AVersion16DatabaseGainsTheReviewColumns() { using var factory = new ServerFactory(); Guid id; using (var db = new NestDatabase(factory.DatabasePath, pooling: false)) id = Submit(db, "before"); using (var connection = new SqliteConnection($"Data Source={factory.DatabasePath};Pooling=False")) { connection.Open(); using var command = connection.CreateCommand(); command.CommandText = """ DROP INDEX ix_nest_jobs_seq; ALTER TABLE nest_jobs DROP COLUMN nestId; ALTER TABLE nest_jobs DROP COLUMN review; ALTER TABLE nest_jobs DROP COLUMN reviewedBy; ALTER TABLE nest_jobs DROP COLUMN reviewedAt; ALTER TABLE nest_jobs DROP COLUMN partsPlaced; ALTER TABLE nest_jobs DROP COLUMN partsUnplaced; ALTER TABLE nest_jobs DROP COLUMN sheets; ALTER TABLE nest_jobs DROP COLUMN utilization; PRAGMA user_version=16; """; command.ExecuteNonQuery(); } using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); var job = upgraded.GetJob(id)!; Assert.Equal("before", job.Title); Assert.Equal(NestJobReview.None, job.Review); Assert.Null(job.Summary); Assert.Single(upgraded.ListJobs(NestJobListFilter.Active, "aj", 10, T0)); } }