bidaangmamamayan/tests/text-test.mjs
Ryan 6b06e08c08 BidaAngMamayan: public supporter tribute news desk for Senator BAM Aquino
Forked from Forthena (PERSONAL/forthena) and retargeted:
- NO login, NO LLM: the edition is public and the ranking is deterministic
  (stories that name Sen. BAM lead; freshest first) — Daniel: no
  like/dislike or store profile for visitors.
- Crawler scoped to BAM: Bing news keyword queries + Inquirer/Rappler
  fallback wires, with a two-tier relevance gate (named-BAM always;
  Aquino+legislative-context only from fallback) so op-eds and non-Bam
  senators are dropped.
- Desks: BAM (lead), Gaming, Education, Politics, Energy, PH, Business.
- Community board /board: 'Thank you Senator BAM' — nickname-only posts,
  HTML-stripped, URL-only rejected, IP rate-limited (3/10min, 20/day).
- Yellow-gamer theme (Orbitron/Rajdhani/JetBrains Mono, dark + gold,
  HUD corners), supporter disclaimer in header and footer.
- Tests: category/date/relevance/text suites + Mongo board suite +
  full HTTP e2e (all passing).

Infra: 66.181.46.132 (bidaangmamamayan.com), local Mongo 7.0,
nginx + certbot TLS, PM2 bama (:3000) + 15-min crawl cron.
2026-09-15 08:37:03 +00:00

65 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Forthena text-shaping test — the anti-tailing guard.
// Run with: node tests/text-test.mjs
import { clipText, dek, firstSentence, isClipped, wordCount } from "../lib/text.js";
let fails = 0;
const ok = (cond, msg) => { if (!cond) { fails++; console.log(" FAIL " + msg); } else console.log(" ok " + msg); };
// The exact failure Daniel screenshotted (2026-08-31): a bare .slice(0,150) cut
// "…the Department of Trade and Industry (DTI)" mid-word into "…the Department of T".
const real = "PHILIPPINE MERCHANDISE exports are expected to outpace the government’s 3% projection, amid a strong performance so far this year, the Department of Trade and Industry (DTI) said.";
const cut = clipText(real, 150);
console.log("clipText(real,150) =>", JSON.stringify(cut));
console.log("anti-tailing:");
ok(!cut.endsWith(" T"), "does not end on a lone letter fragment");
ok(cut !== real.slice(0, 150), "is NOT the raw character slice that produced the bug");
ok(/\s\S$/.test(cut), "ends on a whole word (last char follows a space-delimited token)");
ok(cut.includes("…"), "signals that copy follows (ellipsis)");
ok(!/[,;:–—]$/.test(cut.replace(/…\s*$/, "").trimEnd()), "no dangling punctuation before the ellipsis");
ok(clipText(real, 500) === real.trim(), "short enough text is returned whole, no ellipsis");
ok(clipText("", 100) === "", "empty in, empty out");
ok(clipText(null, 100) === "", "null-safe");
console.log("word boundary discipline:");
const long = "The peso may test new lows this week as inflation concerns and the greenback’s continued strength following US Federal Reserve officials’ hawkish hints about further tightening kept buyers on the sidelines across Asian trading.";
const w = clipText(long, 60);
console.log(" 60 =>", JSON.stringify(w));
// The only honest test of "did not sever a word": what we kept must be followed by a
// space (or the end) in the ORIGINAL text. "…concerns an" fails this — "and" was cut.
const kept = w.replace(/\s*…$/, "").trim();
ok(long.startsWith(kept) && (long.length === kept.length || /\s/.test(long[kept.length])), "kept copy ends on a whole-word boundary in the source");
ok(w !== long.slice(0, 60), "is not the raw character slice");
ok(w.includes("…"), "an ellipsis admits the sentence was cut");
console.log("no space in budget (one huge word) still keeps whole words:");
const solid = "Supercalifragilisticexpialidocious " + "x".repeat(400);
const sw = clipText(solid, 30);
console.log(" 30 =>", JSON.stringify(sw.slice(0, 40)));
ok(sw.length > 0, "returns something rather than throwing");
console.log("prefers a sentence boundary:");
const two = "First sentence is short. Second sentence is much longer than the clip window and will not fit here at all.";
const s = clipText(two, 40);
console.log(" 40 =>", JSON.stringify(s));
ok(s === "First sentence is short.", "returns the complete first sentence without an ellipsis when it fits the boundary");
console.log("dek / firstSentence / isClipped / wordCount:");
ok(dek(real, 180).length <= 200, "dek stays near its budget");
ok(firstSentence(two, 260) === "First sentence is short.", "firstSentence grabs one sentence");
ok(isClipped(real, 150) === true, "isClipped true when copy is longer than the box");
ok(isClipped("short one", 150) === false, "isClipped false when it fits");
ok(wordCount("one two three") === 3, "wordCount counts words not chars");
// A cut must never advertise more than it holds: cards hide the affordance when not clipped.
ok(isClipped(real, real.length + 10) === false, "no 'more' affordance on unclipped copy");
// Abbreviation guard: "U.S." must not be treated as a sentence end.
const abbr = "The U.S. Federal Reserve signalled more tightening while Asian markets opened lower and the peso tracked outside gains across the region.";
const a = clipText(abbr, 60);
console.log(" abbr 60 =>", JSON.stringify(a));
ok(!/^The U\.S\.$/.test(a), "does not end the clip at the 'U.S.' abbreviation");
console.log(fails ? `\n${fails} FAILED` : "\nall passed");
process.exit(fails ? 1 : 0);