using System.Buffers.Binary; using IpPbx.CLMgrLib; using SwyxTray.Recording; using SwyxTray.Swyx; internal static class OutboxChecks { public static void Run(Action check) { var temp = Path.Combine(Path.GetTempPath(), "swyx-outbox-check-" + Guid.NewGuid()); Directory.CreateDirectory(temp); try { for (int scenario = 0; scenario < 7; scenario++) { var folder = Path.Combine(temp, scenario.ToString()); Directory.CreateDirectory(folder); var queue = Path.Combine(folder, "queue"); var now = DateTime.UtcNow; var wav = Path.Combine(folder, "conversation.wav"); WriteWav(Path.Combine(folder, "old.wav"), now.AddDays(-1)); using var box = new RecordingOutbox(queue, () => now); var id = box.Begin(1, 41, "123", "Test", folder, null); check(box.Begin(1, 41, "123", "Test", folder, Guid.NewGuid()) == id, "duplicate start keeps identity"); WriteWav(wav, now); box.Process(); check(box.Notices().Length == 0, "no transfer during recording"); box.Observe(SwyxSnapshot.Offline); check(box.CurrentId(1, 41) == id, "disconnect does not end recording"); if (scenario == 1) WriteWav(Path.Combine(folder, "ambiguous.wav"), now); if (scenario == 2) File.Delete(wav); if (scenario == 3) { // Overlapping recording windows cannot claim the same WAV. File.Delete(wav); box.Begin(2, 42, "456", "Other", folder, null); WriteWav(wav, now); box.End(2, 42); } box.Observe(Snapshot(CLMgrLineStates.LSOnHold)); check(box.CurrentId(1, 41) == id, "hold does not end recording"); box.Observe(Snapshot(CLMgrLineStates.LSInactive)); check(box.CurrentId(1, 41) == null, "call end finishes recording section"); now = now.AddSeconds(4); box.Process(); if (scenario == 1 || scenario == 3) { check(box.Notices().First(n => n.RecordingId == id).State == "error", "ambiguous file is never offered"); continue; } if (scenario == 2) { now = now.AddMinutes(3); box.Process(); check(box.Notices()[0].State == "error", "missing new WAV does not select old file"); box.Acknowledge(id, new string('a', 32)); check(box.Notices()[0].State == "submitted", "manual fallback receipt persists too"); continue; } now = now.AddSeconds(3); if (scenario == 4) { using (var writer = new FileStream(wav, FileMode.Open, FileAccess.Write, FileShare.Read)) { box.Process(); check(box.Notices()[0].State == "waiting", "writer must close before ready"); } } if (scenario == 5) { var content = File.ReadAllBytes(wav); BinaryPrimitives.WriteUInt32LittleEndian(content.AsSpan(4), (uint)content.Length + 100); File.WriteAllBytes(wav, content); box.Process(); now = now.AddSeconds(3); box.Process(); check(box.Notices()[0].State == "waiting", "unfinished RIFF header waits"); WriteWav(wav, now.AddSeconds(-7)); box.Process(); now = now.AddSeconds(3); } if (scenario == 6) { using var restarted = new RecordingOutbox(queue, () => now); restarted.Process(); now = now.AddSeconds(3); restarted.Process(); check(restarted.Notices()[0].State == "ready", "pending section survives tray restart"); continue; } box.Process(); check(box.Notices()[0].State == "ready", "stable closed WAV becomes ready"); using (var input = box.Open(id)) check(input.Length == 2044, "copy contains complete audio"); using (var reloaded = new RecordingOutbox(queue, () => now)) check(reloaded.Notices()[0].State == "ready", "ready replays after restart"); box.Acknowledge(id, new string('a', 32)); box.Acknowledge(id, new string('a', 32)); using var received = new RecordingOutbox(queue, () => now); check(received.Notices()[0].JobId == new string('a', 32), "durable idempotent receipt"); check(File.Exists(wav) && !File.Exists(Path.Combine(queue, id + ".wav")), "remove only acknowledged temporary copy"); } // A call can end outside SwyxWeb. No recording.stop/hangup command // is needed: the same outbox must produce a downloadable WAV. var externalEnds = new[] { ("remote", CLMgrLineStates.LSTerminated, 41), ("local", CLMgrLineStates.LSInactive, 41), ("disabled", CLMgrLineStates.LSDisabled, 41), ("none", CLMgrLineStates.LSNone, 41), ("replaced", CLMgrLineStates.LSRinging, 42), ("reconnected", CLMgrLineStates.LSInactive, 41) }; foreach (var (reason, state, callId) in externalEnds) { var folder = Path.Combine(temp, "external-" + reason); Directory.CreateDirectory(folder); var now = DateTime.UtcNow; using var box = new RecordingOutbox(Path.Combine(folder, "queue"), () => now); var id = box.Begin(1, 41, "123", "Test", folder, null); WriteWav(Path.Combine(folder, "call.wav"), now); box.Observe(Snapshot(CLMgrLineStates.LSActive)); if (reason == "reconnected") { box.Observe(SwyxSnapshot.Offline); check(box.Notices().Length == 0, "connection loss alone does not release audio"); } box.Observe(Snapshot(state, callId)); now = now.AddSeconds(4); box.Process(); now = now.AddSeconds(3); box.Process(); check(box.Notices().Single() is { State: "ready", CallEnded: true }, "automatic handoff without local hangup: " + reason); using var audio = box.Open(id); check(audio.Length == 2044, "complete WAV after external end: " + reason); } var sequential = Path.Combine(temp, "sequential"); Directory.CreateDirectory(sequential); var clock = DateTime.UtcNow; using var sections = new RecordingOutbox(Path.Combine(sequential, "queue"), () => clock); sections.Begin(1, 88, "123", "Test", sequential, null); WriteWav(Path.Combine(sequential, "first.wav"), clock); sections.End(1, 88); clock = clock.AddMilliseconds(100); sections.Begin(1, 88, "123", "Test", sequential, null); WriteWav(Path.Combine(sequential, "second.wav"), clock); sections.End(1, 88); clock = clock.AddSeconds(4); sections.Process(); clock = clock.AddSeconds(3); sections.Process(); check(sections.Notices().Length == 2 && sections.Notices().All(n => n.State == "waiting_call_end" && !n.CallEnded), "all stopped recording sections wait for hangup"); sections.Observe(Snapshot(CLMgrLineStates.LSOnHold, 88)); sections.Observe(SwyxSnapshot.Offline); check(sections.Notices().All(n => n.State == "waiting_call_end"), "hold/disconnect do not release stopped sections"); using (var restarted = new RecordingOutbox(Path.Combine(sequential, "queue"), () => clock)) { restarted.Observe(Snapshot(CLMgrLineStates.LSActive, 88)); check(restarted.Notices().All(n => n.State == "waiting_call_end"), "stopped sections wait across restart"); foreach (var pending in restarted.Notices()) { bool refused = false; try { using var blockedInput = restarted.Open(pending.RecordingId); } catch (InvalidOperationException) { refused = true; } check(refused, "download denied before confirmed call end"); refused = false; try { restarted.Acknowledge(pending.RecordingId, new string('b', 32)); } catch (InvalidOperationException) { refused = true; } check(refused, "receipt denied before confirmed call end"); } restarted.Observe(Snapshot(CLMgrLineStates.LSInactive, 88)); check(restarted.Notices().Length == 2 && restarted.Notices().All(n => n.State == "ready" && n.CallEnded), "hangup releases all finalized sections of the ended call"); using var input = restarted.Open(restarted.Notices()[0].RecordingId); check(input.Length == 2044, "complete WAV available after hangup"); } } finally { Directory.Delete(temp, true); } } public static void WriteWav(string path, DateTime created, int samples = 1000) { using (var writer = new BinaryWriter(File.Create(path))) { writer.Write("RIFF"u8); writer.Write(36 + samples * 2); writer.Write("WAVEfmt "u8); writer.Write(16); writer.Write((short)1); writer.Write((short)1); writer.Write(16000); writer.Write(32000); writer.Write((short)2); writer.Write((short)16); writer.Write("data"u8); writer.Write(samples * 2); writer.Write(new byte[samples * 2]); } File.SetCreationTimeUtc(path, created); } private static SwyxSnapshot Snapshot(CLMgrLineStates state, int callId = 41) => new(true, true, "test", "test", [new SwyxLineInfo(0, state, "123", "Test", true, callId)]); }