From 7b204ad2ac461a92af208c980d27ad5e275e542c Mon Sep 17 00:00:00 2001 From: JusticeWay Date: Sat, 1 Aug 2026 06:01:50 +0500 Subject: [PATCH] enhance(#2530): extend UAT checkpoint frame language pack (9 more languages) (#2564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: extend UAT checkpoint frame language pack (9 more languages) response_language is a free-form config value, but CHECKPOINT_FRAMES only covered 9 languages — any other configured language silently fell back to the English frame. Add Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian frames plus their aliases, with a regression test asserting each resolves instead of falling back. Follow-up to #2402 (PR #2457). Co-Authored-By: Claude Fable 5 * chore: add changeset for #2527 Co-Authored-By: Claude Fable 5 * docs(#2530): list UAT checkpoint frame languages in CONFIGURATION.md Co-Authored-By: Claude Fable 5 * chore(#2530): point changeset fragment at PR #2557 Co-Authored-By: Claude Fable 5 * fix(#2530): address Unicode language-pack review * fix: address checkpoint language review * fix: count spacing combining marks in checkpoint width * test: verify checkpoint aliases structurally * fix: isolate RTL checkpoint frames * fix: isolate RTL checkpoint frames correctly * test(#2530): assert checkpoint aliases neither collide nor go unreachable Review Minor #1. A duplicate alias key was invisible to the existing catalog tests: the runtime object is well-formed after JS collapses the literal, the self-alias assertion still holds, and the losing language just stops resolving. tsc catches the byte-equal case (TS1117), but not the two that survive compilation — an alias whose NFC-lowercase form already belongs to another language, and an alias not in lookup form at all, which resolveCheckpointFrame() can never produce. The check reads the source literal rather than the object, since the object no longer records what was written. Both assertions are independently load-bearing: an NFD twin of an existing alias trips the collision check, an uppercase alias trips the unreachability check. Review Minor #2: changeset retyped Changed -> Added. Nine wholly new supported response_language values are an addition under Keep a Changelog, not a modification of existing behavior. * test(#2530): check alias collisions on the catalog, not its source --------- Co-authored-by: Claude Fable 5 Co-authored-by: Tom Boucher Co-authored-by: Rezolv --- .changeset/brave-wasps-sing.md | 5 + docs/CONFIGURATION.md | 2 +- src/uat.cts | 89 ++++++++-- tests/uat.test.cjs | 294 ++++++++++++++++++++++++++++----- 4 files changed, 333 insertions(+), 57 deletions(-) create mode 100644 .changeset/brave-wasps-sing.md diff --git a/.changeset/brave-wasps-sing.md b/.changeset/brave-wasps-sing.md new file mode 100644 index 000000000..c007b3b86 --- /dev/null +++ b/.changeset/brave-wasps-sing.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 2564 +--- +**UAT checkpoint frames now cover 9 more languages** — `response_language` values of Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, or Indonesian render a localized checkpoint banner/instruction instead of silently falling back to the English frame (#2530). diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8766ab7f5..68dc4288d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -186,7 +186,7 @@ project one is reported, since that is the file you are most likely able to fix. | `dynamic_routing.provider_escalation` | string[] | ordered model IDs | (none) | Opt-in fallback providers tried when a run dies on a quota / rate limit — see [provider escalation](#provider-escalation-on-quota-exceeded--added-in-v143). Added in v1.43 ([#2296](https://github.com/open-gsd/gsd-core/issues/2296)) | | `project_code` | string | any short string | (none) | Prefix for phase directory names (e.g., `"ABC"` produces `ABC-01-setup/`). Added in v1.31 | | `phase_id_convention` | enum | `"milestone-prefixed"`, `null` | `null` | Phase ID naming convention. `null` = legacy numeric IDs (`Phase 1`, `Phase 2`). `"milestone-prefixed"` = globally unique IDs that encode the enclosing milestone (`Phase 1-01`, `Phase 1-02`). Run `gsd-tools roadmap upgrade --convention milestone-prefixed` to migrate an existing ROADMAP.md. | -| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32 | +| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32. UAT checkpoint frames (`/gsd-verify-work`) render a localized banner/instruction for English, Spanish, French, German, Portuguese, Japanese, Chinese, Korean, Italian, Dutch, Polish, Russian, Ukrainian, Turkish, Hindi, Arabic, Vietnamese, and Indonesian (endonyms and ISO codes also accepted); any other value falls back to the English frame. | | `context_window` | number | any integer | `200000` | Context window size in tokens. Set `1000000` for 1M-context models (e.g., `claude-fable-5`). Values `>= 500000` enable adaptive context enrichment (full-body reads of prior SUMMARY.md, deeper anti-pattern reads). Configured via `/gsd-config --advanced`. | | `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 | | `claude_md_path` | string | any file path | `./.claude/CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./.claude/CLAUDE.md` — a valid project-scoped memory location that keeps GSD-generated content from polluting a hand-crafted repo-root `CLAUDE.md` ([#1098](https://github.com/open-gsd/gsd-core/issues/1098)). An existing file without GSD markers is never overwritten unless `--force` is passed. Default changed from `./CLAUDE.md` in v1.5. Added in v1.36 | diff --git a/src/uat.cts b/src/uat.cts index 39c44c6a5..ee346e699 100644 --- a/src/uat.cts +++ b/src/uat.cts @@ -359,6 +359,7 @@ function parseExpectedFromTestBlock(block: string): string | null { interface CheckpointFrame { banner: string; instruction: string; + direction?: 'rtl'; } const CHECKPOINT_BOX_WIDTH = 64; // total column width of the ╔══...╗ border, borders stay byte-identical @@ -400,6 +401,43 @@ const CHECKPOINT_FRAMES: Record = { banner: 'PUNTO DI CONTROLLO: Verifica richiesta', instruction: 'Digita `pass` o descrivi cosa non va.', }, + dutch: { + banner: 'CONTROLEPUNT: Verificatie vereist', + instruction: 'Typ `pass` of beschrijf wat er mis is.', + }, + polish: { + banner: 'PUNKT KONTROLNY: Wymagana weryfikacja', + instruction: 'Wpisz `pass` lub opisz, co jest nie tak.', + }, + russian: { + banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка', + instruction: 'Введите `pass` или опишите, что не так.', + }, + ukrainian: { + banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка', + instruction: 'Введіть `pass` або опишіть, що не так.', + }, + turkish: { + banner: 'KONTROL NOKTASI: Doğrulama gerekli', + instruction: '`pass` yazın veya sorunu açıklayın.', + }, + hindi: { + banner: 'चेकपॉइंट: सत्यापन आवश्यक', + instruction: '`pass` लिखें या बताएं कि क्या गलत है।', + }, + arabic: { + banner: 'نقطة تحقق: المراجعة مطلوبة', + instruction: 'اكتب `pass` أو صف المشكلة.', + direction: 'rtl', + }, + vietnamese: { + banner: 'ĐIỂM KIỂM TRA: Cần xác minh', + instruction: 'Nhập `pass` hoặc mô tả vấn đề.', + }, + indonesian: { + banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan', + instruction: 'Ketik `pass` atau jelaskan apa yang salah.', + }, }; // Free-form response_language aliases → canonical CHECKPOINT_FRAMES key. @@ -413,22 +451,30 @@ const CHECKPOINT_LANGUAGE_ALIASES: Record = { chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese', korean: 'korean', ko: 'korean', '한국어': 'korean', italian: 'italian', it: 'italian', italiano: 'italian', + dutch: 'dutch', nl: 'dutch', nederlands: 'dutch', flemish: 'dutch', vlaams: 'dutch', + polish: 'polish', pl: 'polish', polski: 'polish', + russian: 'russian', ru: 'russian', 'ru-ru': 'russian', 'русский': 'russian', + ukrainian: 'ukrainian', uk: 'ukrainian', ua: 'ukrainian', 'українська': 'ukrainian', + turkish: 'turkish', tr: 'turkish', 'türkçe': 'turkish', turkce: 'turkish', + hindi: 'hindi', hi: 'hindi', 'हिन्दी': 'hindi', 'हिंदी': 'hindi', + arabic: 'arabic', ar: 'arabic', 'العربية': 'arabic', + vietnamese: 'vietnamese', vi: 'vietnamese', 'tiếng việt': 'vietnamese', 'tieng viet': 'vietnamese', + indonesian: 'indonesian', id: 'indonesian', 'bahasa indonesia': 'indonesian', }; function resolveCheckpointFrame(responseLanguage: string | undefined): CheckpointFrame { if (!responseLanguage) return CHECKPOINT_FRAMES.english; - const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()]; + const key = CHECKPOINT_LANGUAGE_ALIASES[ + responseLanguage.trim().normalize('NFC').toLowerCase() + ]; return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english; } -// Approximate East Asian Width ranges (Unicode property values W and F) — the -// CJK scripts CHECKPOINT_FRAMES ships (Japanese/Chinese/Korean) render each -// matching code point at 2 terminal/display columns, not 1. Padding computed -// from `.length` (UTF-16 code units) undercounts these by one column per -// wide character, visually misaligning the box's right border (#2402 review -// medium finding). Latin-script frames (English/Spanish/French/German/ -// Portuguese/Italian) contain no wide code points, so displayWidth === length -// for them — no behavior change there. +// Approximate terminal-cell width. East Asian Width W/F code points occupy two +// cells, while Unicode combining marks occupy no additional cell beyond their +// base character. Counting only W/F ranges is insufficient for scripts such as +// Devanagari: Hindi vowel signs and viramas are combining marks, and treating +// each as a full cell visibly shifts the checkpoint box's right border. function isWideCodePoint(codePoint: number): boolean { return ( (codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo @@ -447,11 +493,17 @@ function isWideCodePoint(codePoint: number): boolean { ); } +// Non-spacing/enclosing marks and format controls occupy zero terminal cells. +// Spacing combining marks (General_Category=Mc), such as Devanagari vowel +// signs, still advance the cursor and must contribute one column. +const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u; + // Iterates by Unicode code point (not UTF-16 code unit) so astral characters // are measured once, not as two surrogate units. function displayWidth(text: string): number { let width = 0; for (const ch of text) { + if (ZERO_WIDTH_MARK_RE.test(ch)) continue; width += isWideCodePoint(ch.codePointAt(0) as number) ? 2 : 1; } return width; @@ -468,11 +520,22 @@ function checkpointBoxLine(text: string): string { return `║${padded}║`; } +const RTL_ISOLATE = '\u2067'; +const POP_DIRECTIONAL_ISOLATE = '\u2069'; + +function isolateCheckpointFrameText(text: string, frame: CheckpointFrame): string { + return frame.direction === 'rtl' + ? `${RTL_ISOLATE}${text}${POP_DIRECTIONAL_ISOLATE}` + : text; +} + function buildCheckpoint(currentTest: { number: number; name: string; expected: string }, responseLanguage?: string): string { const frame = resolveCheckpointFrame(responseLanguage); + const banner = isolateCheckpointFrameText(frame.banner, frame); + const instruction = isolateCheckpointFrameText(frame.instruction, frame); return [ '╔══════════════════════════════════════════════════════════════╗', - checkpointBoxLine(frame.banner), + checkpointBoxLine(banner), '╚══════════════════════════════════════════════════════════════╝', '', `**Test ${currentTest.number}: ${currentTest.name}**`, @@ -480,7 +543,7 @@ function buildCheckpoint(currentTest: { number: number; name: string; expected: currentTest.expected, '', '──────────────────────────────────────────────────────────────', - frame.instruction, + instruction, '──────────────────────────────────────────────────────────────', ].join('\n'); } @@ -953,5 +1016,9 @@ export = { cmdRenderCheckpoint, parseCurrentTest, buildCheckpoint, + CHECKPOINT_FRAMES, + CHECKPOINT_LANGUAGE_ALIASES, + resolveCheckpointFrame, + checkpointBoxLine, parseDeferredItems, }; diff --git a/tests/uat.test.cjs b/tests/uat.test.cjs index 65a55b8b4..9b0d7936f 100644 --- a/tests/uat.test.cjs +++ b/tests/uat.test.cjs @@ -8,8 +8,15 @@ const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); +const fc = require('./helpers/fast-check-setup.cjs'); const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); -const { buildCheckpoint } = require('../gsd-core/bin/lib/uat.cjs'); +const { + buildCheckpoint, + CHECKPOINT_FRAMES, + CHECKPOINT_LANGUAGE_ALIASES, + resolveCheckpointFrame, + checkpointBoxLine, +} = require('../gsd-core/bin/lib/uat.cjs'); describe('audit-uat command', () => { let tmpDir; @@ -861,54 +868,233 @@ describe('uat render-checkpoint', () => { assert.notStrictEqual(japanese, english); }); - // Regression: #2402 review medium finding — checkpointBoxLine() padded using - // JS string `.length` (UTF-16 code units), not display width. Japanese/ - // Chinese/Korean use full-width characters that render at 2 terminal - // columns each, so the padded line was JS-length-correct (64) but visually - // 8-15 columns too wide, misaligning the right `║` border relative to the - // box's single-width border lines. Independently recomputes display width - // (East Asian Width W/F ranges) rather than reading source, so this test - // fails if the fix regresses even if the banner copy itself later changes. - describe('CJK checkpoint banner padding uses display width, not UTF-16 length (#2402)', () => { - function isWideCodePoint(codePoint) { - return ( - (codePoint >= 0x1100 && codePoint <= 0x115f) || - codePoint === 0x2329 || codePoint === 0x232a || - (codePoint >= 0x2e80 && codePoint <= 0x303e) || - (codePoint >= 0x3041 && codePoint <= 0x33ff) || - (codePoint >= 0x3400 && codePoint <= 0x4dbf) || - (codePoint >= 0x4e00 && codePoint <= 0x9fff) || - (codePoint >= 0xa000 && codePoint <= 0xa4cf) || - (codePoint >= 0xac00 && codePoint <= 0xd7a3) || - (codePoint >= 0xf900 && codePoint <= 0xfaff) || - (codePoint >= 0xfe30 && codePoint <= 0xfe4f) || - (codePoint >= 0xff00 && codePoint <= 0xff60) || - (codePoint >= 0xffe0 && codePoint <= 0xffe6) || - (codePoint >= 0x20000 && codePoint <= 0x3fffd) + test('resolveCheckpointFrame: every extended-pack alias resolves its localized frame', () => { + // Exercise canonical names, ISO codes, endonyms, and transliterations so a + // typo or duplicate alias cannot silently route a supported language back + // to the English fallback. + const cases = [ + { + aliases: ['Dutch', 'nl', 'nederlands', 'flemish', 'vlaams'], + frame: { + banner: 'CONTROLEPUNT: Verificatie vereist', + instruction: 'Typ `pass` of beschrijf wat er mis is.', + }, + }, + { + aliases: ['Polish', 'pl', 'polski'], + frame: { + banner: 'PUNKT KONTROLNY: Wymagana weryfikacja', + instruction: 'Wpisz `pass` lub opisz, co jest nie tak.', + }, + }, + { + aliases: ['Russian', 'ru', 'ru-ru', 'русский'], + frame: { + banner: 'КОНТРОЛЬНАЯ ТОЧКА: требуется проверка', + instruction: 'Введите `pass` или опишите, что не так.', + }, + }, + { + aliases: ['Ukrainian', 'uk', 'ua', 'українська'], + frame: { + banner: 'КОНТРОЛЬНА ТОЧКА: потрібна перевірка', + instruction: 'Введіть `pass` або опишіть, що не так.', + }, + }, + { + aliases: ['Turkish', 'tr', 'türkçe', 'turkce'], + frame: { + banner: 'KONTROL NOKTASI: Doğrulama gerekli', + instruction: '`pass` yazın veya sorunu açıklayın.', + }, + }, + { + aliases: ['Hindi', 'hi', 'हिन्दी', 'हिंदी'], + frame: { + banner: 'चेकपॉइंट: सत्यापन आवश्यक', + instruction: '`pass` लिखें या बताएं कि क्या गलत है।', + }, + }, + { + aliases: ['Arabic', 'ar', 'العربية'], + frame: { + banner: 'نقطة تحقق: المراجعة مطلوبة', + instruction: 'اكتب `pass` أو صف المشكلة.', + direction: 'rtl', + }, + }, + { + aliases: ['Vietnamese', 'vi', 'tiếng việt', 'tieng viet'], + frame: { + banner: 'ĐIỂM KIỂM TRA: Cần xác minh', + instruction: 'Nhập `pass` hoặc mô tả vấn đề.', + }, + }, + { + aliases: ['Indonesian', 'id', 'bahasa indonesia'], + frame: { + banner: 'TITIK PEMERIKSAAN: Verifikasi diperlukan', + instruction: 'Ketik `pass` atau jelaskan apa yang salah.', + }, + }, + ]; + for (const { aliases, frame } of cases) { + for (const alias of aliases) { + assert.deepStrictEqual( + resolveCheckpointFrame(alias), + frame, + `${alias} resolved to the wrong checkpoint frame`, + ); + } + } + }); + + test('checkpoint frame and alias catalogs remain structurally complete', () => { + const english = CHECKPOINT_FRAMES.english; + assert.ok(english, 'English fallback frame must exist'); + + for (const [language, frame] of Object.entries(CHECKPOINT_FRAMES)) { + const expectedKeys = frame.direction + ? ['banner', 'direction', 'instruction'] + : ['banner', 'instruction']; + assert.deepStrictEqual( + Object.keys(frame).sort(), + expectedKeys, + `${language} has an unexpected checkpoint-frame shape`, ); - } - function displayWidth(text) { - let width = 0; - for (const ch of text) width += isWideCodePoint(ch.codePointAt(0)) ? 2 : 1; - return width; + assert.ok(frame.banner.trim(), `${language} banner must be non-empty`); + assert.ok(frame.instruction.trim(), `${language} instruction must be non-empty`); + if (frame.direction !== undefined) { + assert.strictEqual(frame.direction, 'rtl', `${language} has an unsupported direction`); + } + assert.strictEqual( + CHECKPOINT_LANGUAGE_ALIASES[language], + language, + `${language} must self-alias to its canonical frame`, + ); + if (language !== 'english') { + assert.notDeepStrictEqual(frame, english, `${language} must not duplicate the English frame`); + } } - for (const lang of ['Japanese', 'Chinese', 'Korean']) { - test(`${lang} checkpoint banner line renders at display-width 64, aligning the right border`, () => { - const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' }; - const output = buildCheckpoint(currentTest, lang); - const lines = output.split('\n'); - const topBorder = lines[0]; - const bannerLine = lines[1]; - const bottomBorder = lines[2]; - - assert.strictEqual(displayWidth(topBorder), 64, 'top border is the 64-column reference width'); - assert.strictEqual(displayWidth(bottomBorder), 64, 'bottom border is the 64-column reference width'); - assert.strictEqual(displayWidth(bannerLine), 64, - `${lang} banner line must render at the same 64-column display width as the borders — ` + - 'padding by UTF-16 .length under-pads full-width characters and overflows the box'); - }); + for (const [alias, language] of Object.entries(CHECKPOINT_LANGUAGE_ALIASES)) { + const frame = CHECKPOINT_FRAMES[language]; + assert.ok(frame, `${alias} targets missing checkpoint frame ${language}`); + assert.strictEqual( + resolveCheckpointFrame(alias), + frame, + `${alias} must resolve to its declared checkpoint frame`, + ); + if (language !== 'english') { + assert.notDeepStrictEqual( + frame, + english, + `${alias} must not resolve to the English fallback`, + ); + } } + }); + + // Two alias keys that differ only by case or Unicode normalization form are + // distinct object keys — every assertion above still passes. But resolution + // lowercases and NFC-normalizes before the lookup, so the two collapse to one + // lookup key at runtime and whichever was written first becomes unreachable: + // the losing language silently renders the English fallback. + // + // Both defects survive compilation and both are observable on the catalog + // itself, precisely because the keys stay distinct. The remaining case — two + // byte-identical keys, where the object genuinely no longer records what was + // written — is rejected by tsc as TS1117 before this suite can run, since the + // tests execute against `gsd-core/bin/lib/uat.cjs` built from this source. + test('checkpoint alias catalog declares no colliding or unreachable alias keys', () => { + const declared = Object.keys(CHECKPOINT_LANGUAGE_ALIASES); + + const seen = new Set(); + const collisions = declared.filter( + (alias) => seen.size === seen.add(alias.normalize('NFC').toLowerCase()).size, + ); + assert.deepStrictEqual( + collisions, + [], + `alias key(s) collapse onto an earlier alias once normalized for lookup, so one language silently loses its alias: ${collisions.join(', ')}`, + ); + + // An alias not already in lookup form is the mirror defect: it collides with + // nothing, and resolveCheckpointFrame() — which normalizes its argument + // before indexing — can never produce it, so the entry is simply dead. + const unreachable = declared.filter( + (alias) => alias !== alias.normalize('NFC').toLowerCase(), + ); + assert.deepStrictEqual( + unreachable, + [], + `alias key(s) are not in NFC-lowercase lookup form and can never resolve: ${unreachable.join(', ')}`, + ); + }); + + test('resolveCheckpointFrame: canonically equivalent aliases resolve after NFC normalization', () => { + assert.deepStrictEqual( + resolveCheckpointFrame('türkçe'.normalize('NFD')), + resolveCheckpointFrame('türkçe'), + ); + assert.deepStrictEqual( + resolveCheckpointFrame('tiếng việt'.normalize('NFD')), + resolveCheckpointFrame('tiếng việt'), + ); + }); + + // Regression: #2402 review medium finding — checkpointBoxLine() padded using + // JS string `.length` (UTF-16 code units), not display width. The property + // below supplies an independent, category-labelled cell-width oracle rather + // than copying the implementation's Unicode range logic. + describe('checkpoint banner padding uses terminal display width (#2402, #2530)', () => { + test('property: category-labelled strings are padded to a 62-cell interior', () => { + const oneCell = fc.constantFrom( + { text: 'a', width: 1 }, + { text: '7', width: 1 }, + { text: ' ', width: 1 }, + { text: '\u093e', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN AA + { text: '\u093f', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN I + { text: '\u0949', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN CANDRA O + ); + const zeroCell = fc.constantFrom( + { text: '\u0301', width: 0 }, // Mn: COMBINING ACUTE ACCENT + { text: '\u093c', width: 0 }, // Mn: DEVANAGARI SIGN NUKTA + { text: '\u20dd', width: 0 }, // Me: COMBINING ENCLOSING CIRCLE + { text: '\u200d', width: 0 }, // Cf: ZERO WIDTH JOINER + { text: '\u2066', width: 0 }, // Cf: LEFT-TO-RIGHT ISOLATE + { text: '\u2069', width: 0 }, // Cf: POP DIRECTIONAL ISOLATE + ); + const twoCell = fc.constantFrom( + { text: '界', width: 2 }, + { text: '語', width: 2 }, + { text: '한', width: 2 }, + ); + + fc.assert(fc.property( + fc.array(fc.oneof(oneCell, zeroCell, twoCell), { maxLength: 35 }), + (cells) => { + const text = cells.map((cell) => cell.text).join(''); + const textWidth = cells.reduce((sum, cell) => sum + cell.width, 0); + const padding = ' '.repeat(Math.max(0, 60 - textWidth)); + assert.strictEqual( + checkpointBoxLine(text), + `║ ${text}${padding}║`, + ); + }, + )); + }); + + test('padding boundary: width limit-1, limit, and limit+1', () => { + for (const width of [59, 60, 61]) { + const text = 'a'.repeat(width); + assert.strictEqual( + checkpointBoxLine(text), + `║ ${text}${' '.repeat(Math.max(0, 60 - width))}║`, + `unexpected rendering at text width ${width}`, + ); + } + }); test('exact rendered banner lines for Japanese/Chinese/Korean (regression pin)', () => { const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' }; @@ -925,6 +1111,24 @@ describe('uat render-checkpoint', () => { '║ 체크포인트: 검증 필요 ║', ); }); + + test('exact rendered Hindi banner line ignores combining-mark cell width (regression pin)', () => { + const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' }; + assert.strictEqual( + buildCheckpoint(currentTest, 'Hindi').split('\n')[1], + `║ चेकपॉइंट: सत्यापन आवश्यक${' '.repeat(40)}║`, + ); + }); + + test('exact rendered Arabic frame is isolated inside the LTR checkpoint layout', () => { + const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' }; + const arabic = buildCheckpoint(currentTest, 'Arabic'); + assert.strictEqual( + arabic.split('\n')[1], + `║ \u2067نقطة تحقق: المراجعة مطلوبة\u2069${' '.repeat(34)}║`, + ); + assert.ok(arabic.includes('\u2067اكتب `pass` أو صف المشكلة.\u2069')); + }); }); test('renders the current checkpoint as raw output', () => {