Prd/components/backend/test/test_wav_split.rb
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

118 lines
5.2 KiB
Ruby

# frozen_string_literal: true
# Offline tests for the stereo call-recording splitter (no gems, no fixtures - WAVs are built here).
# This handles the only copy of a recorded call, so the tests that matter most are the ones proving it
# refuses to guess: wrong channel count, wrong bit depth, not a WAV at all. Mangling audio silently
# would be worse than declining to split it.
# Run: ruby components/backend/test/test_wav_split.rb
require_relative "../lib/helpdesk/wav_split"
require "tmpdir"
$pass = 0; $fail = 0
def ok(d); $pass += 1; puts " ok #{d}"; end
def bad(d, got = nil); $fail += 1; puts " FAIL #{d}#{got.nil? ? '' : " (got: #{got.inspect})"}"; end
def eq(a, b, d); a == b ? ok(d) : bad(d, a); end
def truthy(x, d); x ? ok(d) : bad(d, x); end
def falsy(x, d); x ? bad(d, x) : ok(d); end
def raises(d) ; yield; bad(d, "no error"); rescue Helpdesk::WavSplit::Error; ok(d); end
W = Helpdesk::WavSplit
# Build a stereo WAV from explicit per-channel samples, so the test knows exactly what should come out.
def stereo_wav(left, right, rate: 16_000, bits: 16, channels: 2, fmt: 1, extra_chunk: false)
pcm = +""
left.zip(right).each { |l, r| pcm << [l].pack("v") << [r].pack("v") }
body = +""
body << "fmt " << [16].pack("V") << [fmt].pack("v") << [channels].pack("v")
body << [rate].pack("V") << [rate * channels * bits / 8].pack("V")
body << [channels * bits / 8].pack("v") << [bits].pack("v")
body << "LIST" << [4].pack("V") << "INFO" if extra_chunk
body << "data" << [pcm.bytesize].pack("V") << pcm
"RIFF" + [4 + body.bytesize].pack("V") + "WAVE" + body
end
def samples_of(path)
raw = File.binread(path)
info = W.parse(raw)
raw.byteslice(info[:data_offset], info[:data_size]).unpack("v*")
end
puts "parsing a header"
h = W.parse(stereo_wav([1, 2], [3, 4]))
eq(h[:channels], 2, "channel count read")
eq(h[:sample_rate], 16_000, "sample rate read")
eq(h[:bits], 16, "bit depth read")
eq(h[:format], 1, "PCM format read")
truthy(h[:data_offset] > 0, "data offset located")
puts "chunks in any order, unknown ones skipped"
h2 = W.parse(stereo_wav([1], [2], extra_chunk: true))
eq(h2[:channels], 2, "a LIST chunk before data does not confuse it")
truthy(h2[:data_offset] > 0, "data still found after an unknown chunk")
puts "splitting puts each speaker in their own file"
Dir.mktmpdir do |dir|
src = File.join(dir, "call.wav")
# left = operator, right = caller; distinct values so a swap would be obvious
File.binwrite(src, stereo_wav([100, 101, 102], [-100, -101, -102]))
l = File.join(dir, "op.wav"); r = File.join(dir, "caller.wav")
res = W.split(src, left_path: l, right_path: r)
eq(res[:frames], 3, "frame count reported")
eq(res[:sample_rate], 16_000, "sample rate carried through")
eq(samples_of(l), [100, 101, 102], "left channel is the operator, in order")
eq(samples_of(r), [65_436, 65_435, 65_434], "right channel is the caller (unsigned view of -100..-102)")
eq(W.parse(File.binread(l))[:channels], 1, "output is mono")
eq(W.parse(File.binread(r))[:channels], 1, "both outputs are mono")
eq(W.parse(File.binread(l))[:sample_rate], 16_000, "output keeps the sample rate")
end
puts "a truncated recording is split as far as it goes"
Dir.mktmpdir do |dir|
src = File.join(dir, "cut.wav")
full = stereo_wav([1, 2, 3, 4], [5, 6, 7, 8])
# A call that ended abruptly: the header still claims the original length.
File.binwrite(src, full.byteslice(0, full.bytesize - 8))
l = File.join(dir, "l.wav"); r = File.join(dir, "r.wav")
res = W.split(src, left_path: l, right_path: r)
eq(res[:frames], 2, "only the frames actually present are used")
eq(samples_of(l), [1, 2], "left truncated cleanly")
eq(samples_of(r), [5, 6], "right truncated cleanly")
end
puts "IT MUST REFUSE what it does not fully understand"
Dir.mktmpdir do |dir|
l = File.join(dir, "l.wav"); r = File.join(dir, "r.wav")
mono = File.join(dir, "mono.wav")
File.binwrite(mono, stereo_wav([1, 2], [3, 4], channels: 1))
raises("a mono file is refused, not silently duplicated") { W.split(mono, left_path: l, right_path: r) }
deep = File.join(dir, "24bit.wav")
File.binwrite(deep, stereo_wav([1], [2], bits: 24))
raises("24-bit audio is refused rather than misread as 16") { W.split(deep, left_path: l, right_path: r) }
junk = File.join(dir, "junk.wav")
File.binwrite(junk, "this is not audio at all")
raises("a non-RIFF file is refused") { W.split(junk, left_path: l, right_path: r) }
empty = File.join(dir, "empty.wav")
File.binwrite(empty, stereo_wav([], []))
raises("a file with no frames is refused") { W.split(empty, left_path: l, right_path: r) }
falsy(File.exist?(l), "nothing is written when the input is rejected")
end
puts "splittable? answers without raising"
Dir.mktmpdir do |dir|
good = File.join(dir, "s.wav"); File.binwrite(good, stereo_wav([1], [2]))
mono = File.join(dir, "m.wav"); File.binwrite(mono, stereo_wav([1], [2], channels: 1))
junk = File.join(dir, "j.wav"); File.binwrite(junk, "nope")
truthy(W.splittable?(good), "stereo PCM is splittable")
falsy(W.splittable?(mono), "mono is not")
falsy(W.splittable?(junk), "junk is not, and does not raise")
falsy(W.splittable?(File.join(dir, "missing.wav")), "a missing file is not, and does not raise")
end
puts
puts "#{$pass} passed, #{$fail} failed"
exit($fail.zero? ? 0 : 1)