libs/mastodon-typed-text-test.lua

136 lines

1-- mastodon-typed-text-test.lua
2-- Unit tests for libs/mastodon-typed-text.lua (issue 4-003, August 2026).
3-- Pure string-in/string-out, runs standalone with no project setup:
4-- luajit libs/mastodon-typed-text-test.lua
5-- The final section is an optional integration pass: if the extracted
6-- fediverse poems.json is present, three poems whose golden status was
7-- hand-verified during the audit are recounted from their raw HTML.
8
9-- {{{ setup_dir_path()
10local function setup_dir_path(provided)
11 if provided then return provided end
12 return "/mnt/mtwo/programming/ai-stuff/neocities-modernization"
13end
14-- }}}
15
16local DIR = setup_dir_path(arg and arg[1])
17package.path = DIR .. "/libs/?.lua;" .. package.path
18local typed = require("mastodon-typed-text")
19
20local passed, failed = 0, 0
21
22-- {{{ local function check_equal()
23local function check_equal(name, got, want)
24 if got == want then
25 passed = passed + 1
26 print(" ok " .. name)
27 else
28 failed = failed + 1
29 print("FAIL " .. name)
30 print(" want: " .. tostring(want))
31 print(" got: " .. tostring(got))
32 end
33end
34-- }}}
35
36-- Emphasis delimiters come back (the golden poem deficit mechanism)
37check_equal("em restores asterisks",
38 typed.restore("<p>I'd <em>love</em> to talk</p>"),
39 "I'd *love* to talk")
40check_equal("strong restores double asterisks",
41 typed.restore("<p>a <strong>bold</strong> claim</p>"),
42 "a **bold** claim")
43check_equal("del restores tildes",
44 typed.restore("<p>never <del>always</del> mind</p>"),
45 "never ~~always~~ mind")
46check_equal("code restores backticks",
47 typed.restore("<p>run <code>make</code> twice</p>"),
48 "run `make` twice")
49
50-- Tag-name literals must not bleed into longer tags
51check_equal("<b> restoration leaves <br /> alone",
52 typed.restore("<p>one<br />two <b>loud</b></p>"),
53 "one\ntwo **loud**")
54check_equal("<s> restoration leaves <span> alone",
55 typed.restore('<p><span class="x">kept</span> <s>gone</s></p>'),
56 "kept ~~gone~~")
57
58-- Paragraph structure: each break was two typed newlines
59check_equal("paragraphs become blank lines",
60 typed.restore("<p>first</p><p>second</p>"),
61 "first\n\nsecond")
62
63-- Entity decoding: specific entities before &amp;, so typed "&lt;" survives
64check_equal("typed &lt; literal survives decoding",
65 typed.restore("<p>type &amp;lt; for less-than</p>"),
66 "type &lt; for less-than")
67check_equal("plain ampersand decodes",
68 typed.restore("<p>salt &amp; pepper</p>"),
69 "salt & pepper")
70
71-- Typed backslash-escapes are the author's text, not markup damage
72check_equal("backslash-quote sequences survive",
73 typed.restore("<p>the \\&quot;or\\&quot; operator</p>"),
74 "the \\\"or\\\" operator")
75
76-- Compose box length: visible characters, not bytes or UTF-16 units
77check_equal("ascii counts one per char", typed.composer_length("abc"), 3)
78check_equal("curly quote counts 1 (not 3 bytes)", typed.composer_length("\226\128\153"), 1)
79check_equal("astral emoji counts 1", typed.composer_length("\240\159\152\128"), 1)
80check_equal("heart plus variation selector counts 1",
81 typed.composer_length("\226\157\164\239\184\143"), 1)
82
83-- Mentions charge their visible text; URLs charge a flat 23
84check_equal("mention counts as visible @user",
85 typed.compose_box_count('<p><span class="h-card"><a href="https://spore.social/@friend" class="u-url mention">@<span>friend</span></a></span> hi</p>', nil),
86 #"@friend hi")
87check_equal("hashtag counts as visible #tag",
88 typed.compose_box_count('<p><a href="https://tech.lgbt/tags/poetry" class="mention hashtag" rel="tag">#<span>poetry</span></a></p>', nil),
89 #"#poetry")
90check_equal("long URL counts as 23",
91 typed.compose_box_count('<p>see <a href="https://example.com/a/very/long/path"><span class="invisible">https://</span><span class="ellipsis">example.com/a/very</span><span class="invisible">/long/path</span></a></p>', nil),
92 #"see " + 23)
93check_equal("short URL also counts as 23",
94 typed.compose_box_count('<p><a href="https://a.io"><span class="invisible">https://</span>a.io</a></p>', nil),
95 23)
96check_equal("plain-text mention prices at local part",
97 typed.compose_box_count("<p>@friend@octodon.social hello</p>", nil),
98 #"@friend hello")
99
100-- Content warning text joins the count, delimiters join the count
101check_equal("cw text adds to the count",
102 typed.compose_box_count("<p>body</p>", "warn"),
103 #"body" + #"warn")
104check_equal("delimiters join the count",
105 typed.compose_box_count("<p>I'd <em>love</em> to</p>", nil),
106 #"I'd *love* to")
107
108-- {{{ Optional integration: recount audit-verified poems from the archive
109local dkjson_ok, dkjson = pcall(require, "dkjson")
110local poems_path = DIR .. "/input/fediverse/files/poems.json"
111local pf = io.open(poems_path, "r")
112if dkjson_ok and pf then
113 local data = dkjson.decode(pf:read("*a"))
114 pf:close()
115 -- Poems hand-verified during the August 2026 audit. 0100/0145/4129 lost
116 -- exactly their markdown delimiters; 2066 has a never-linkified plain-text
117 -- mention whose domain rides free; 1673 has a heart emoji with an
118 -- invisible variation selector. Typed length was 1024 on the nose for all.
119 local verified_golden = {
120 ["0100"] = true, ["0145"] = true, ["4129"] = true,
121 ["2066"] = true, ["1673"] = true,
122 }
123 for _, poem in ipairs(data.poems) do
124 if poem.category == "fediverse" and verified_golden[poem.id] then
125 check_equal("archive poem " .. poem.id .. " recounts to 1024",
126 typed.compose_box_count(poem.raw_content, poem.content_warning), 1024)
127 end
128 end
129else
130 print(" (skipping archive integration checks: poems.json not present)")
131end
132-- }}}
133
134print(string.format("\n%d passed, %d failed", passed, failed))
135os.exit(failed == 0 and 0 or 1)
136