115 lines
5.4 KiB
C#

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<string>();
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<PrintJobState> 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<PendingPrintResult?> 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; }
}