117 lines
8.2 KiB
JavaScript
117 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-describe-group — the packet's generated profile description, asserted per level, per missing
|
|
* field and per locale.
|
|
*
|
|
* WHY A TEST AND NOT A GLANCE: this string is generated for ~2,000 people groups in eleven languages
|
|
* and printed ~20 to a page. Nobody proofreads two thousand of them, so every failure mode here is
|
|
* silent — a missing field blanks a row, a wrong GSEC branch states the wrong fact about a real people,
|
|
* and an English fragment in an Arabic packet reads as a bug in the data rather than in the code.
|
|
*
|
|
* Headless, no network, no map. Strings are INJECTED, so this tests the function and not the catalogue.
|
|
* Exit 0 pass · 1 fail.
|
|
*/
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const { describeGroup } = await import(resolve(ROOT, 'library/packets/describeGroup.js'))
|
|
|
|
// Two locales is enough to prove the lookup: one that exists, one that does not (falls back to en).
|
|
const S = {
|
|
en: {
|
|
desc_where_both: 'The {people} live in {country} and speak {language}.',
|
|
desc_where_country: 'The {people} live in {country}.',
|
|
desc_where_language: 'The {people} speak {language}.',
|
|
desc_gsec_0: 'There are no known evangelical Christians among them.',
|
|
desc_gsec_1: 'Fewer than 2% are evangelical, and no one has been planting churches.',
|
|
desc_gsec_4: 'Between 2% and 5% are evangelical.',
|
|
desc_gsec_6: 'More than 10% are evangelical.',
|
|
},
|
|
ar: {
|
|
desc_where_both: 'يعيش {people} في {country} ويتحدثون {language}.',
|
|
desc_where_country: 'يعيش {people} في {country}.',
|
|
desc_gsec_1: 'أقل من 2% إنجيليون.',
|
|
},
|
|
}
|
|
// The country resolver is INJECTED (see describeGroup's header: countryName.js is Vite-alias-only and
|
|
// imports JSON without an attribute, so importing it here would make this gate unrunnable). A two-entry
|
|
// stub proves the wiring and the localization; the ladder below proves what happens without it.
|
|
const CN = (iso, lc) => ({ MYS: { en: 'Malaysia', ar: 'ماليزيا' } }[iso] || {})[lc] || ''
|
|
const O = { strings: S, countryName: CN }
|
|
const G = { name: 'Abai Sungai', country_code: 'MYS', primary_language_label: 'Abai Sungai', imb_gsec: 1 }
|
|
const fails = []
|
|
const eq = (what, got, want) => { if (got !== want) fails.push(`${what}\n got: ${JSON.stringify(got)}\n want: ${JSON.stringify(want)}`) }
|
|
const has = (what, got, sub) => { if (!String(got).includes(sub)) fails.push(`${what}: ${JSON.stringify(sub)} missing from ${JSON.stringify(got)}`) }
|
|
const hasNot = (what, got, sub) => { if (String(got).includes(sub)) fails.push(`${what}: ${JSON.stringify(sub)} SHOULD NOT be in ${JSON.stringify(got)}`) }
|
|
|
|
// 1. every GSEC level the catalogue defines resolves to ITS OWN sentence
|
|
for (const [g, frag] of [[0, 'no known evangelical'], [1, 'no one has been planting'], [4, 'Between 2% and 5%'], [6, 'More than 10%']]) {
|
|
has(`gsec ${g} picks its own level sentence`, describeGroup({ ...G, imb_gsec: g }, 'en', O), frag)
|
|
}
|
|
eq('gsec 1 in full', describeGroup(G, 'en', O),
|
|
'The Abai Sungai live in Malaysia and speak Abai Sungai. Fewer than 2% are evangelical, and no one has been planting churches.')
|
|
|
|
// 2. the country is resolved to the READER's language from its code, not left English
|
|
has('country localized from the code (ar)', describeGroup(G, 'ar', O), 'ماليزيا')
|
|
hasNot('and the English name is gone', describeGroup(G, 'ar', O), 'Malaysia')
|
|
|
|
// 3. a missing clause is DROPPED, never blanked — the row still describes the people
|
|
const noLang = { ...G }; delete noLang.primary_language_label
|
|
eq('language missing -> country sentence only', describeGroup(noLang, 'en', O),
|
|
'The Abai Sungai live in Malaysia. Fewer than 2% are evangelical, and no one has been planting churches.')
|
|
const noCountry = { ...G }; delete noCountry.country_code
|
|
eq('country missing -> language sentence only', describeGroup(noCountry, 'en', O),
|
|
'The Abai Sungai speak Abai Sungai. Fewer than 2% are evangelical, and no one has been planting churches.')
|
|
has('level survives when both clauses are gone', describeGroup({ imb_gsec: 4 }, 'en', O), 'Between 2% and 5%')
|
|
|
|
// 3b. THE LANGUAGE LABEL IS INDEX-ORDERED AND MUST BE UN-INVERTED BEFORE IT IS PRINTED.
|
|
// The API returns "Arabic, Shihhi"; the sentence has to read "Shihhi Arabic". 725 of the 12,379 rows
|
|
// carry an inverted label, so this is the single most-printed transformation in the description.
|
|
const shihhi = describeGroup({ ...G, name: 'Shihuh', primary_language_label: 'Arabic, Shihhi' }, 'en', O)
|
|
has('inverted label reads naturally', shihhi, 'speak Shihhi Arabic.')
|
|
hasNot('and the raw index ordering is NOT printed', shihhi, 'Arabic, Shihhi')
|
|
// A comma INSIDE a parenthetical is not an inversion marker — 13 real names would be shredded by a
|
|
// naive split ("Central Sulawesi) Boano (Indonesia"). These must pass through untouched.
|
|
has('parenthetical comma is left alone', describeGroup({ ...G, primary_language_label: 'Boano (Indonesia, Central Sulawesi)' }, 'en', O), 'speak Boano (Indonesia, Central Sulawesi).')
|
|
has('a trailing parenthetical stays at the end', describeGroup({ ...G, primary_language_label: 'Naga, Tangkhul (India)' }, 'en', O), 'speak Tangkhul Naga (India).')
|
|
has('an uninverted label is unchanged', describeGroup({ ...G, primary_language_label: 'Ainu (China)' }, 'en', O), 'speak Ainu (China).')
|
|
|
|
// 3c. ★ THE FLATTENED CODE IS NEVER PRINTED. normalize.js flattens primary_language to its .value, an
|
|
// ISO 639-3 code; reading that field would print "and speak abf". A group carrying ONLY the code must
|
|
// fall to the country sentence — dropping the clause, which is the rule, rather than printing a code.
|
|
const codeOnly = { ...G }; delete codeOnly.primary_language_label; codeOnly.primary_language = 'abf'
|
|
eq('a bare language code is dropped, not printed', describeGroup(codeOnly, 'en', O),
|
|
'The Abai Sungai live in Malaysia. Fewer than 2% are evangelical, and no one has been planting churches.')
|
|
|
|
// 3d. NO RESOLVER: the clause degrades down countryName.js's own ladder instead of blanking.
|
|
eq('no resolver, no label -> the alpha-3 code, and the level still states its fact', describeGroup(G, 'ar', { strings: S }),
|
|
'يعيش Abai Sungai في MYS ويتحدثون Abai Sungai. أقل من 2% إنجيليون.')
|
|
eq('no resolver, label present -> the label', describeGroup({ ...G, country_code_label: 'Malaysia' }, 'en', { strings: S }),
|
|
'The Abai Sungai live in Malaysia and speak Abai Sungai. Fewer than 2% are evangelical, and no one has been planting churches.')
|
|
has('resolver returning nothing falls through to the code', describeGroup(G, 'en', { strings: S, countryName: () => '' }), 'live in MYS')
|
|
|
|
// 4. missing or nonsense GSEC omits the level rather than inventing one or throwing
|
|
hasNot('no gsec -> no level sentence', describeGroup({ ...G, imb_gsec: undefined }, 'en', O), 'evangelical')
|
|
hasNot('gsec 9 is not a level', describeGroup({ ...G, imb_gsec: 9 }, 'en', O), 'evangelical')
|
|
eq('an empty group is an empty string, not a crash', describeGroup({}, 'en', O), '')
|
|
eq('a null group is an empty string', describeGroup(null, 'en', O), '')
|
|
|
|
// 5. a locale the catalogue lacks falls back to English rather than printing the key
|
|
has('unknown locale falls back to en', describeGroup(G, 'zz', O), 'The Abai Sungai live in')
|
|
has('partial locale falls back PER KEY', describeGroup(G, 'ar', O), 'أقل من 2%')
|
|
|
|
// 6. truncation cuts at a word boundary and only when asked
|
|
const long = describeGroup(G, 'en', O)
|
|
eq('no cap -> untouched', describeGroup(G, 'en', { ...O, maxChars: 0 }), long)
|
|
const cut = describeGroup(G, 'en', { ...O, maxChars: 40 })
|
|
if (cut.length > 40) fails.push(`maxChars 40 honoured: got ${cut.length}`)
|
|
if (!cut.endsWith('…')) fails.push(`truncation is marked: ${JSON.stringify(cut)}`)
|
|
if (/\s$/.test(cut.slice(0, -1))) fails.push(`truncation trims its tail: ${JSON.stringify(cut)}`)
|
|
|
|
if (fails.length) {
|
|
console.error('DESCRIBE-GROUP: FAIL')
|
|
for (const f of fails) console.error(` ✗ ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log('DESCRIBE-GROUP: PASS — every GSEC level states its own fact, an index-ordered language label is un-inverted without shredding a parenthetical, a missing clause is dropped rather than blanked, the country is localized from its code, and an unknown locale falls back per key.')
|