115 lines
5.4 KiB
C#
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; }
|
|
}
|