Prd/components/backend/test/test_ledger.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

104 lines
6.6 KiB
Ruby

# frozen_string_literal: true
# Zero-dependency offline tests for the pure ledger reducer. Run: ruby components/backend/test/test_ledger.rb
require_relative "../lib/helpdesk/ledger"
$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
L = Helpdesk::Ledger
IDS = (1..9999).to_a
def mint; -> { "led#{IDS.shift}" }; end
# deterministic tiny vectors so cosine is predictable; entry_vec resolves from the delta that created it.
def d(entry_id:, decision:, q: "q", vec: nil, quote: "")
{ "entry_id" => entry_id, "question" => q, "decision" => decision, "quote" => quote, "question_vec" => vec }
end
# resolver: entries created in these tests store their raw vec under vec_ref["_test_vec"] for lookup
def resolver; ->(e) { e.dig("vec_ref", "_test_vec") }; end
puts "packing round-trips"
v = [0.1, -0.25, 1.0, 0.0]
eq(L.unpack_vec(L.pack_vec(v)).map { |x| x.round(4) }, v, "pack/unpack Float32 round-trips (to 4dp)")
truthy(L.pack_vec(v).is_a?(String) && !L.pack_vec(v).include?("\n"), "packed vec is a single-line string")
puts "cosine"
eq(L.cosine([1,0], [1,0]), 1.0, "identical -> 1.0")
eq(L.cosine([1,0], [0,1]), 0.0, "orthogonal -> 0.0")
eq(L.cosine([1,0], [1,2,3]), 0.0, "dim mismatch -> 0.0 (never raises)")
puts "canonical entry_id keying"
r = L.apply(ledger: [], deltas: [d(entry_id: nil, decision: "no", q: "reset?")],
event_id: 1, started_at: "2026-07-01T09:00:00Z", applied_event_ids: [], entry_vec: resolver, next_id: mint)
eq(r[:ledger].size, 1, "first delta creates one entry")
eq(r[:ledger][0]["decision"], "no", "decision recorded")
eq(r[:ledger][0]["last_event_id"], 1, "last_event_id set")
truthy(r[:changed], "changed=true on new entry")
led = r[:ledger]; eid = led[0]["id"]
r2 = L.apply(ledger: led, deltas: [d(entry_id: eid, decision: "no", q: "reset again?")],
event_id: 2, started_at: "2026-07-02T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
eq(r2[:ledger].size, 1, "same entry_id + same decision -> refresh, no new entry")
eq(r2[:ledger][0]["last_event_id"], 2, "refresh updates last_event_id")
puts "never flip a past no: later reversal supersedes + flags, old kept"
r3 = L.apply(ledger: r2[:ledger], deltas: [d(entry_id: eid, decision: "yes")],
event_id: 3, started_at: "2026-07-03T09:00:00Z", applied_event_ids: [1, 2], entry_vec: resolver, next_id: mint)
eq(r3[:ledger].size, 2, "reversal APPENDS (old retained), never mutates in place")
old = r3[:ledger].find { |e| e["id"] == eid }
neu = r3[:ledger].find { |e| e["id"] != eid }
eq(old["decision"], "no", "old 'no' entry unchanged")
eq(old["superseded_by"], neu["id"], "old entry superseded_by the new one")
truthy(old["contradiction"] && neu["contradiction"], "both flagged contradiction")
eq(neu["decision"], "yes", "new entry carries the new decision")
puts "out-of-order (older) reversal does NOT supersede the current decision"
r4 = L.apply(ledger: r2[:ledger], deltas: [d(entry_id: eid, decision: "yes")],
event_id: 4, started_at: "2026-06-01T09:00:00Z", applied_event_ids: [1, 2], entry_vec: resolver, next_id: mint)
older = r4[:ledger].find { |e| e["id"] == eid }
eq(older["decision"], "no", "current 'no' stays current when the reversing event is OLDER")
truthy(older["superseded_by"].nil?, "older reversal does not supersede")
truthy(r4[:ledger].any? { |e| e["contradiction"] }, "older reversal still recorded + flagged for audit")
puts "idempotency per event_id"
r5 = L.apply(ledger: r2[:ledger], deltas: [d(entry_id: eid, decision: "yes")],
event_id: 2, started_at: "2026-07-02T09:00:00Z", applied_event_ids: [1, 2], entry_vec: resolver, next_id: mint)
eq(r5[:ledger].size, 1, "re-applying an already-applied event_id is a no-op")
truthy(!r5[:changed], "no-op apply reports changed=false")
puts "cosine backstop: null entry_id, near-match same decision -> refresh"
base = L.apply(ledger: [], deltas: [d(entry_id: nil, decision: "no", vec: [1.0, 0.0])],
event_id: 1, started_at: "2026-07-01T09:00:00Z", applied_event_ids: [], entry_vec: nil, next_id: mint)[:ledger]
base[0]["vec_ref"] = { "_test_vec" => [1.0, 0.0] } # stash the vec so the resolver can find it
r6 = L.apply(ledger: base, deltas: [d(entry_id: nil, decision: "no", vec: [0.98, 0.02])],
event_id: 2, started_at: "2026-07-02T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
eq(r6[:ledger].size, 1, "near-match (cosine>=0.86) same decision -> refresh, no new entry")
puts "cosine backstop: near-match DIFFERENT decision -> contradiction"
r7 = L.apply(ledger: base, deltas: [d(entry_id: nil, decision: "yes", vec: [0.98, 0.02])],
event_id: 3, started_at: "2026-07-03T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
eq(r7[:ledger].size, 2, "near-match different decision -> append")
truthy(r7[:ledger].all? { |e| e["contradiction"] }, "both flagged contradiction")
puts "cosine below threshold -> new entry; nil vec -> new + flagged"
r8 = L.apply(ledger: base, deltas: [d(entry_id: nil, decision: "no", vec: [0.0, 1.0])],
event_id: 4, started_at: "2026-07-04T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
eq(r8[:ledger].size, 2, "far vector -> distinct new entry")
r9 = L.apply(ledger: base, deltas: [d(entry_id: nil, decision: "no", vec: nil)],
event_id: 5, started_at: "2026-07-05T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
neu9 = r9[:ledger].find { |e| e["last_event_id"] == 5 }
truthy(neu9 && neu9["contradiction"], "embed-failed (nil vec) new entry flagged for review, never silently merged")
puts "unknown/foreign entry_id falls back to the null path (never nil-derefs)"
r10 = L.apply(ledger: base, deltas: [d(entry_id: "does-not-exist", decision: "no", vec: [0.0, 1.0])],
event_id: 6, started_at: "2026-07-06T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
eq(r10[:ledger].size, 2, "unknown entry_id -> treated as null -> new entry (no crash)")
puts "FIX 6: foreign entry_id + nil vec -> new entry flagged contradiction"
r11 = L.apply(ledger: base, deltas: [d(entry_id: "does-not-exist", decision: "no", vec: nil)],
event_id: 7, started_at: "2026-07-07T09:00:00Z", applied_event_ids: [1], entry_vec: resolver, next_id: mint)
neu11 = r11[:ledger].find { |e| e["last_event_id"] == 7 }
truthy(neu11 && neu11["contradiction"], "foreign entry_id + nil question_vec -> new entry flagged contradiction")
puts "\n#{$pass} passed, #{$fail} failed"
exit($fail.zero? ? 0 : 1)