using System.Text.Json; namespace Zcbot.WindowsNode.Tests; public sealed class JobOfferPersistenceTests : IDisposable { private readonly string root = Path.Combine( Path.GetTempPath(), "zcbot-job-offer-tests", Guid.NewGuid().ToString("N")); [Fact] public void AcceptedOfferIsDurableAndAConflictingDigestIsRejected() { var jobId = Guid.NewGuid(); var leaseId = Guid.NewGuid(); var digest = new string('d', 64); var inbox = new JobInboxStore(root); var adapters = new AdapterCatalog([new AcceptingAdapter()]); var accepted = inbox.Accept(Offer(jobId, leaseId, digest), adapters); Assert.True(accepted.Accepted); var requestPath = Path.Combine( root, jobId.ToString("D"), "request", "request.json"); var statePath = Path.Combine(root, jobId.ToString("D"), "state.json"); Assert.True(File.Exists(requestPath)); Assert.True(File.Exists(statePath)); using (var request = JsonDocument.Parse(File.ReadAllBytes(requestPath))) { Assert.Equal( digest, request.RootElement.GetProperty("request_digest").GetString()); Assert.Equal( leaseId, request.RootElement.GetProperty("lease_id").GetGuid()); } using (var state = JsonDocument.Parse(File.ReadAllBytes(statePath))) { Assert.Equal("accepted", state.RootElement.GetProperty("stage").GetString()); } var conflict = inbox.Accept( Offer(jobId, Guid.NewGuid(), new string('e', 64)), adapters); Assert.False(conflict.Accepted); Assert.Equal("job_digest_conflict", conflict.Reason); } [Fact] public void SameDigestWithANewLeaseUpdatesOnlyThePersistedOfferIdentity() { var jobId = Guid.NewGuid(); var firstLease = Guid.NewGuid(); var secondLease = Guid.NewGuid(); var digest = new string('f', 64); var inbox = new JobInboxStore(root); var adapters = new AdapterCatalog([new AcceptingAdapter()]); Assert.True(inbox.Accept(Offer(jobId, firstLease, digest), adapters).Accepted); var replay = inbox.Accept(Offer(jobId, secondLease, digest), adapters); Assert.True(replay.Accepted); using var request = JsonDocument.Parse(File.ReadAllBytes(Path.Combine( root, jobId.ToString("D"), "request", "request.json"))); Assert.Equal(secondLease, request.RootElement.GetProperty("lease_id").GetGuid()); Assert.Equal(digest, request.RootElement.GetProperty("request_digest").GetString()); } public void Dispose() { if (Directory.Exists(root)) { Directory.Delete(root, recursive: true); } } private static JsonElement Offer(Guid jobId, Guid leaseId, string digest) { var payload = JsonSerializer.SerializeToElement(new { job_id = jobId, lease_id = leaseId, request_digest = digest, capability = "test.capability@v1", request = new { inputs = Array.Empty() }, workspace = (object?)null, input_transfers = Array.Empty(), }); return payload; } private sealed class AcceptingAdapter : INodeAdapter { public string Capability => "test.capability@v1"; public string DisplayName => "Test"; public string AdapterVersion => "1.0.0"; public IReadOnlyList Features => []; public bool HasActiveJobs => false; public string RunningDetail => "running"; public string? RuntimeId => null; public bool SupportsLocalAcceptance => false; public string RuntimePath => "test.exe"; public string ContractPath => "contract.json"; public string ContractSha256 => new('0', 64); public string? WorkspaceStateFilename => null; public IReadOnlyList PreviewOutputIds => []; public AdapterRuntimeStatus DetectRuntime() => new("Test", "1", AdapterVersion, "ready", "ready"); public void InvalidateRuntime() { } public bool ValidateRequest(JsonElement request) => true; public Task RunAsync(RecoverableJob job) => Task.CompletedTask; public void Cancel(Guid jobId) { } public Task RunLocalAcceptanceAsync( string workRoot, IProgress progress, CancellationToken cancellationToken) => throw new NotSupportedException(); } }