using FixedAssetPrintAgent; var configuration = new AgentConfiguration { ServerUrl = "https://asset.example.com", TerminalId = 1 }; var job = new PrintJobDto { Id = 7, ClaimToken = "attempt-one", JobType = "asset_label" }; var journal = new MemoryJournal(); var statuses = new List(); var prints = 0; var failCompleted = true; Task Report(int id, string token, string status, string? error) { Check(id == 7 && token == "attempt-one", "每次回写必须绑定原领取凭据"); statuses.Add(status); if (status == "completed" && failCompleted) { failCompleted = false; throw new IOException("network disconnected"); } return Task.CompletedTask; } Task Read(int id) => Task.FromResult(new PrintJobState { Id = id, Status = "result_unknown", ClaimToken = job.ClaimToken }); Task Dispatch() { prints++; return Task.CompletedTask; } try { await PrintResultRecovery.ExecuteAsync(job, configuration, journal, Report, Read, Dispatch, _ => { }); throw new Exception("应模拟完成回写断网"); } catch (IOException) { } Check(journal.Pending?.Phase == "completed_pending_ack", "打印成功回写断网仍须保留completed"); await PrintResultRecovery.AcknowledgeAsync(journal.Pending!, journal, Report, Read, _ => { }); Check(prints == 1 && statuses.SequenceEqual(new[] { "printing", "completed", "completed" }), "只能补交结果,不能重印或改报失败"); Check(journal.Pending is null, "确认后清理恢复记录"); Console.WriteLine("PASS completion acknowledgment retries without printing"); foreach (var phase in new[] { "dispatching", "claimed", "completed_pending_ack" }) { var pending = new PendingPrintResult { JobId = 7, ClaimToken = job.ClaimToken, Phase = phase }; await journal.SaveAsync(pending); await PrintResultRecovery.AcknowledgeAsync(pending, journal, Report, Read, _ => { }); Check(statuses.Last() == (phase == "completed_pending_ack" ? "completed" : "result_unknown"), "恢复阶段不得猜测是否已打印"); } Console.WriteLine("PASS restart recovery does not dispatch"); await PrintResultRecovery.ExecuteAsync(job, configuration, journal, Report, Read, () => throw new InvalidOperationException("partial print failed"), _ => { }); Check(statuses.Last() == "result_unknown", "部分出纸异常必须待核实"); Console.WriteLine("PASS partial dispatch failure remains uncertain"); journal.FailSave = true; try { await PrintResultRecovery.ExecuteAsync(job, configuration, journal, Report, Read, Dispatch, _ => { }); throw new Exception("应模拟落盘失败"); } catch (IOException) { } Check(prints == 1, "落盘失败禁止打印"); journal.FailSave = false; Console.WriteLine("PASS journal write failure prevents printing"); var conflict = new PendingPrintResult { JobId = 7, ClaimToken = job.ClaimToken, Phase = "completed_pending_ack" }; await journal.SaveAsync(conflict); Task ConflictReport(int id, string token, string status, string? error) => throw new PrintAgentRequestException(409, "conflict"); try { await PrintResultRecovery.AcknowledgeAsync(conflict, journal, ConflictReport, Read, _ => { }); throw new Exception("同token未确定结果必须保留恢复记录"); } catch (PrintAgentRequestException) { } Check(journal.Pending is not null, "409不能直接清记录"); await PrintResultRecovery.AcknowledgeAsync(conflict, journal, ConflictReport, _ => Task.FromResult(new PrintJobState { Id = 7, Status = "queued", ClaimToken = null }), _ => { }); Check(journal.Pending is null, "管理员已重新派单后允许释放旧记录"); Console.WriteLine("PASS conflict inspection preserves unresolved attempts"); foreach (var invalid in new[] { new PrintJobState(), new PrintJobState { Status = "completed" }, new PrintJobState { Status = "bogus", ClaimToken = null } }) { await journal.SaveAsync(conflict); try { await PrintResultRecovery.AcknowledgeAsync(conflict, journal, ConflictReport, _ => Task.FromResult(invalid), _ => { }); throw new Exception("无效状态读回不得确认完成"); } catch (InvalidOperationException) { } Check(journal.Pending is not null, "缺状态或token必须保留记录"); } Console.WriteLine("PASS invalid server response retains recovery journal"); var temporaryRoot = Path.Combine(Path.GetTempPath(), $"asset-print-journal-{Guid.NewGuid():N}"); try { var disk = new PrintResultJournal(temporaryRoot); await disk.SaveAsync(conflict); Check((await new PrintResultJournal(temporaryRoot).LoadAsync())?.Phase == conflict.Phase, "落盘记录必须跨实例恢复"); await disk.ClearAsync(); Check(await disk.LoadAsync() is null, "清记录后不应留旧任务"); await File.WriteAllTextAsync(Path.Combine(temporaryRoot, "pending-print-result.json"), "{bad"); try { await disk.LoadAsync(); throw new Exception("损坏记录应阻止自动打印"); } catch (System.Text.Json.JsonException) { } } finally { Directory.Delete(temporaryRoot, true); } Console.WriteLine("PASS durable journal restoration and corruption protection"); static void Check(bool condition, string message) { if (!condition) throw new Exception(message); } internal sealed class MemoryJournal : IPrintResultJournal { public PendingPrintResult? Pending; public bool FailSave; public Task LoadAsync() => Task.FromResult(Pending); public Task SaveAsync(PendingPrintResult result) { if (FailSave) throw new IOException("disk full"); Pending = result; return Task.CompletedTask; } public Task ClearAsync() { Pending = null; return Task.CompletedTask; } }