using System.Text.Json; namespace Zcbot.WindowsNode.Tests; public sealed class JobRepositoryCompatibilityTests : IDisposable { private readonly string root = Path.Combine( Path.GetTempPath(), "zcbot-job-repository-tests", Guid.NewGuid().ToString("N")); [Fact] public void ReadsExistingRequestAndStateWithoutMigration() { var jobId = Guid.NewGuid(); var leaseId = Guid.NewGuid(); var directory = Path.Combine(root, jobId.ToString("D")); Directory.CreateDirectory(Path.Combine(directory, "request")); File.WriteAllText( Path.Combine(directory, "request", "request.json"), JsonSerializer.Serialize(new { job_id = jobId, lease_id = leaseId, request_digest = new string('b', 64), capability = "origin.plot@v2", accepted_at = "2026-08-20T01:02:03Z", request = new { }, workspace = (object?)null, input_transfers = Array.Empty(), })); File.WriteAllText( Path.Combine(directory, "state.json"), """{"stage":"software_running","progress":10,"detail":"running","updated_at":"2026-08-20T01:03:00Z"}"""); var monitor = new JobMonitorService(root); var repository = new JobRepository(root); var recovered = Assert.Single(repository.ReadRecoverableJobs()); var displayed = Assert.Single(monitor.ReadSnapshots()); Assert.Equal(jobId, recovered.JobId); Assert.Equal(leaseId, recovered.LeaseId); Assert.Equal("software_running", displayed.Stage); Assert.False(recovered.UploadComplete); } [Fact] public void RecoveryTransitionPreservesTheExistingStateJsonShape() { var job = CreateJob(); var directory = Path.Combine(root, job.JobId.ToString("D")); Directory.CreateDirectory(directory); File.WriteAllText( Path.Combine(directory, "state.json"), """{"stage":"software_running","progress":10,"detail":"running","updated_at":"2026-08-20T01:03:00Z"}"""); var repository = new JobRepository(root); Assert.Throws(() => repository.WriteState( job, "downloading_inputs", 0, "retry")); repository.WriteState( job, "downloading_inputs", 0, "retry", JobTransitionMode.Recovery); using var state = JsonDocument.Parse( File.ReadAllBytes(Path.Combine(directory, "state.json"))); Assert.Equal("downloading_inputs", state.RootElement.GetProperty("stage").GetString()); Assert.Equal(0, state.RootElement.GetProperty("progress").GetInt32()); Assert.Equal("retry", state.RootElement.GetProperty("detail").GetString()); Assert.True(state.RootElement.TryGetProperty("updated_at", out _)); Assert.Equal(4, state.RootElement.EnumerateObject().Count()); } [Fact] public void TerminalWriteRemainsIdempotent() { var job = CreateJob(); var repository = new JobRepository(root); repository.WriteTerminal(job, "failed", "FIRST", "first failure"); repository.WriteTerminal(job, "cancelled", "SECOND", "late cancellation"); using var terminal = JsonDocument.Parse(File.ReadAllBytes(Path.Combine( root, job.JobId.ToString("D"), "terminal.json"))); Assert.Equal("failed", terminal.RootElement.GetProperty("status").GetString()); Assert.Equal( "FIRST", terminal.RootElement.GetProperty("error").GetProperty("code").GetString()); } public void Dispose() { if (Directory.Exists(root)) { Directory.Delete(root, recursive: true); } } private static RecoverableJob CreateJob() => new( Guid.NewGuid(), Guid.NewGuid(), new string('c', 64), "origin.plot@v2", null, null, null, false, false); }