# frozen_string_literal: true # Offline tests for the transcript glossary (no gems, no network). # This code REWRITES what a caller was told, so the tests that matter most are the ones proving it # refuses to act: on ordinary words, on distant words, on text that was already correct, and on the # transcript's own structure. A glossary that corrupts a good transcript is worse than none. # Run: ruby components/backend/test/test_glossary.rb require_relative "../lib/helpdesk/glossary" $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 G = Helpdesk::Glossary TERMS = %w[yamt Ježlovice repeater Vodafone].freeze def fix(text, terms = TERMS) = G.correct(text, terms) puts "the case this exists for: a homophone no model can spell" out, ch = fix("Zkusil jste reset brány přes jamt?") eq(out, "Zkusil jste reset brány přes yamt?", "jamt -> yamt") eq(ch, [{ from: "jamt", to: "yamt" }], "and the substitution is reported, not silent") puts "it reports what it did, because a transcript is evidence" _, ch = fix("jamt a Jezlovice") eq(ch.length, 2, "every change is listed") truthy(ch.all? { |c| c[:from] && c[:to] }, "each with both the old and new word") puts "diacritics are forgiven when matching, but the term's own spelling is written" eq(fix("Přístup na Jezlovice")[0], "Přístup na Ježlovice", "missing accents still match") puts "punctuation survives" eq(fix("Přes jamt, prosím.")[0], "Přes yamt, prosím.", "trailing comma kept") eq(fix("(jamt)")[0], "(yamt)", "brackets kept") eq(fix("jamt.")[0], "yamt.", "full stop kept") puts "IT MUST REFUSE: already-correct text is left exactly alone" %w[yamt Ježlovice repeater].each do |w| out, ch = fix("Tady je #{w} a nic víc") eq(out, "Tady je #{w} a nic víc", "#{w} untouched when already right") eq(ch, [], "and nothing reported for #{w}") end puts "the hazard that the matcher CANNOT solve, and which the curated list must prevent" # Honest about the limit: "malíček" (little finger) is one edit from "balíček", differing at the first # letter exactly like the "jamt"/"yamt" case this tool exists for. Nothing in the text distinguishes # them, so if a common word were ever listed, correct Czech WOULD be rewritten. This test documents # that rather than pretending otherwise - the protection is the list, enforced further down. eq(fix("bolí mě malíček", %w[balíček])[0], "bolí mě balíček", "a listed COMMON word does corrupt ordinary text - which is why common words are banned") eq(fix("máme tady kabel", TERMS)[0], "máme tady kabel", "an unrelated word is untouched") eq(fix("prosím pošlete to", TERMS)[0], "prosím pošlete to", "ordinary sentence untouched") puts "IT MUST REFUSE: capitalisation, which is not a misrecognition" # Caught on a real call: the glossary rewrote "Repeater" at the start of a sentence to the lower-case # listed form, damaging correct text. Spelling is this tool's business; case is not. eq(fix("Repeater je nainstalovaný")[0], "Repeater je nainstalovaný", "a sentence-initial term keeps its capital") eq(fix("ježlovice")[0], "ježlovice", "and a lower-case proper noun is not force-capitalised either") eq(fix("YAMT")[0], "YAMT", "shouting is left alone too") # Accents are more than case, so they are still restored. eq(fix("Jezlovice")[0], "Ježlovice", "but a missing accent is still fixed") puts "IT MUST REFUSE: Czech inflection, which is grammar and not a mistake" eq(fix("jsme na Vodafonu")[0], "jsme na Vodafonu", "the locative of Vodafone is left alone") eq(fix("přes yamtu")[0], "přes yamtu", "an inflected yamt is not forced back to nominative") eq(fix("do Ježlovic")[0], "do Ježlovic", "genitive plural of a site name survives") truthy(G.suffix_only_difference?("vodafonu", "vodafone"), "ending-only difference is recognised as inflection") falsy(G.suffix_only_difference?("jamt", "yamt"), "a first-letter difference is NOT inflection") puts "IT MUST REFUSE: words too far away to be the same word" eq(fix("javor")[0], "javor", "javor is not a mangled yamt") eq(fix("nemam")[0], "nemam", "unrelated short word untouched") eq(fix("Jihlava")[0], "Jihlava", "a different place name is not Ježlovice") puts "IT MUST REFUSE: the transcript's own structure" line = "[00:00:07] [Speaker 2]: Zkusili jsme jamt." out, = fix(line) eq(out, "[00:00:07] [Speaker 2]: Zkusili jsme yamt.", "words fixed, timestamp and speaker tag intact") truthy(out.start_with?("[00:00:07] [Speaker 2]:"), "the prefix is byte-identical") multi = "[00:00:01] [Speaker 1]: jamt\n[00:00:05] [Speaker 2]: Jezlovice\n" eq(fix(multi)[0], "[00:00:01] [Speaker 1]: yamt\n[00:00:05] [Speaker 2]: Ježlovice\n", "every line handled, newlines preserved") puts "a term swallowed by the preposition before it" # From a real call: "přes yamt" came back as "přezjamt" - fused AND voice-assimilated, so the token # never resembles the term on its own and the plain matcher could not see it. eq(fix("Zkusil jste reset brány přezjamt?")[0], "Zkusil jste reset brány přez yamt?", "přezjamt is split back into preposition + term") eq(fix("reset brány přesyamt")[0], "reset brány přes yamt", "unassimilated form too") eq(fix("na Jezlovice")[0], "na Ježlovice", "a separate preposition is untouched, the term still fixed") _, ch = fix("přezjamt") eq(ch.first[:to], "přez yamt", "the change is reported as the split") puts "IT MUST REFUSE to hack a term out of an ordinary word" # The guard is that the part before the term has to be a real preposition, not merely short. eq(fix("dynamt")[0], "dynamt", "dyn+amt is not a preposition, left alone") eq(fix("abcjamt")[0], "abcjamt", "arbitrary letters before the term are not a split") eq(fix("nejamtovitost")[0], "nejamtovitost", "the term must be at the END of the token") eq(fix("krajamt")[0], "krajamt", "kra is not a preposition") puts "multi-word terms match as a phrase" mw = ["RF plánovač"] eq(fix("souhlas od RF planovac", mw)[0], "souhlas od RF plánovač", "two-word term matched and corrected") eq(fix("RF plánovač to zamítl", mw)[0], "RF plánovač to zamítl", "already correct phrase untouched") puts "edge cases do not blow up" eq(fix("")[0], "", "empty transcript") eq(fix("nic", [])[0], "nic", "no terms configured -> unchanged") eq(G.correct(nil, TERMS)[0], nil, "nil in, nil out") eq(fix(" ")[0], " ", "whitespace only") puts "loading the shipped list" terms = G.load truthy(terms.include?("yamt"), "yamt is in the repo glossary") truthy(terms.include?("Ježlovice"), "Ježlovice is in the repo glossary") truthy(terms.none? { |t| t.gsub(/\s+/, "").length < G::MIN_TERM_LEN }, "no term is short enough to be risky") falsy(terms.any? { |t| t.start_with?("#") }, "comments are stripped") eq(G.load("/nonexistent/glossary.txt"), [], "a missing file is not fatal") # Regression: the terms are Czech, so reading them under the ambient locale meant a non-UTF-8 LANG # produced an EMPTY glossary rather than an error anyone would notice. truthy(G.load.all? { |t| t.encoding == Encoding::UTF_8 }, "terms are read as UTF-8 whatever the locale") truthy(G.load.any? { |t| t.match?(/[ěščřžýáíéúůňťď]/i) }, "and the accented terms actually survive loading") puts "the shipped list contains no ordinary-word landmines" # If someone adds a common word later, this is the test that should start failing. %w[balíček zmeškaný hovor adresa problém].each do |common| falsy(G.load.any? { |t| G.send(:near_miss?, common, t) || G.fold(t) == G.fold(common) }, "#{common} is not within reach of any shipped term") end puts puts "#{$pass} passed, #{$fail} failed" exit($fail.zero? ? 0 : 1)