diff --git a/.github/scripts/tests/test_scheduler_empty_messages.py b/.github/scripts/tests/test_scheduler_empty_messages.py index 7d80faf8..7214ffcd 100644 --- a/.github/scripts/tests/test_scheduler_empty_messages.py +++ b/.github/scripts/tests/test_scheduler_empty_messages.py @@ -35,7 +35,8 @@ def consumers(): def fixture(directory, backend='pbs', consumer='attached', rows='', second=None, locale='en', catalog=None, real_dialog=None): directory = Path(directory) - (directory / 'xx.json').write_text(json.dumps(catalog or {})) + if catalog is not None: + (directory / f'{locale}.json').write_text(json.dumps(catalog), encoding='utf-8') (directory / 'rows').write_text(rows) (directory / 'second').write_text(rows if second is None else second) bindir = directory / 'bin' @@ -51,7 +52,7 @@ FIXTURE={shlex.quote(str(directory))} BACKEND={backend} HB_UI_MENU_H=20 HB_UI_MENU_W=84 HB_UI_MENU_LIST=10 command_not_found_handle() {{ printf 'FORBIDDEN:%s\\n' "$*" >> "$FIXTURE/errors"; return 99; }} -jq() {{ [[ "$#" == 6 && "$1" == -r && "$2" == --arg && "$3" == text && "$5" == '.[$text] // empty' && "$6" == "$FIXTURE/xx.json" ]] || {{ printf 'bad jq' >> "$FIXTURE/errors"; return 99; }}; command jq "$@"; }} +jq() {{ [[ "$#" == 6 && "$1" == -r && "$2" == --arg && "$3" == text && "$5" == '.[$text] // empty' && "$6" == "$FIXTURE/$LANGUAGE.json" ]] || {{ printf 'bad jq' >> "$FIXTURE/errors"; return 99; }}; command jq "$@"; }} head() {{ [[ "$*" == '-1' ]] || return 99; local first; IFS= read -r first; printf '%s\\n' "$first"; while IFS= read -r first; do :; done; }} hb_pve_list_vzdump_jobs() {{ local file="$FIXTURE/rows" line @@ -107,20 +108,40 @@ class SchedulerMessages(unittest.TestCase): self.assertEqual(result['status'], '1') def test_lookup_cache_modes_at_both_consumers(self): - shipped = json.loads((ROOT / 'lang/it.json').read_text()) + # Unconditional synthetic coverage, independent of cache completeness. for backend, message in MESSAGES.items(): for consumer in ('attached', 'new'): - for locale, catalog, expected in ( - ('en', {message: 'ignored'}, message), - ('missing', {}, message), - ('xx', {'unrelated': 'unrelated'}, message), - ('xx', shipped, shipped.get(message, message)), - ('xx', {message: 'SYNTHETIC reordered backend: ' + backend}, - 'SYNTHETIC reordered backend: ' + backend), + for mode, locale, catalog, expected, hint in ( + ('english', 'en', {message: 'ignored', HINT: 'ignored'}, message, HINT), + ('missing-file', 'missing', None, message, HINT), + ('missing-key', 'xx', {'unrelated': 'unrelated'}, message, HINT), + ('translated', 'xx', {message: 'SYNTHETIC reordered backend: ' + backend, + HINT: 'SYNTHETIC hint'}, + 'SYNTHETIC reordered backend: ' + backend, 'SYNTHETIC hint'), ): - with self.subTest(backend=backend, consumer=consumer, locale=locale, expected=expected): + with self.subTest(backend=backend, consumer=consumer, mode=mode): result = run_case(backend=backend, consumer=consumer, locale=locale, catalog=catalog) - suffix = '\n\n' + catalog.get(HINT, HINT) if consumer == 'new' else '' + suffix = '\n\n' + hint if consumer == 'new' else '' + self.assertEqual(result['message'], expected + suffix) + self.assertEqual(result['status'], '1') + + def test_all_shipped_caches_at_both_consumers(self): + catalogs = sorted((ROOT / 'lang').glob('*.json')) + self.assertTrue(catalogs, 'No shipped locale catalogs discovered') + for path in catalogs: + catalog = json.loads(path.read_text(encoding='utf-8')) + self.assertIsInstance(catalog, dict, path.name) + for backend, message in MESSAGES.items(): + for consumer in ('attached', 'new'): + with self.subTest(locale=path.stem, backend=backend, consumer=consumer): + # Exercise the real translate function with the actual locale + # filename, not just a comparison of JSON values. Missing or + # empty translations are valid: runtime falls back to English. + result = run_case(backend=backend, consumer=consumer, + locale=path.stem, catalog=catalog) + expected = message if path.stem == 'en' else catalog.get(message) or message + hint = HINT if path.stem == 'en' else catalog.get(HINT) or HINT + suffix = '\n\n' + hint if consumer == 'new' else '' self.assertEqual(result['message'], expected + suffix) self.assertEqual(result['status'], '1') diff --git a/.github/workflows/test-i18n.yml b/.github/workflows/test-i18n.yml index c5b4bff7..2456ec66 100644 --- a/.github/workflows/test-i18n.yml +++ b/.github/workflows/test-i18n.yml @@ -10,6 +10,10 @@ on: - 'AppImage/lib/i18n/**' - 'AppImage/components/terminal-panel.tsx' - 'CONTRIBUTING.md' + # Scheduler consumers and the offline extractor's .sh/.func inputs. + - 'scripts/**/*.sh' + - 'scripts/**/*.func' + - 'lang/*.json' push: branches: [main, develop] paths: @@ -20,6 +24,10 @@ on: - 'AppImage/lib/i18n/**' - 'AppImage/components/terminal-panel.tsx' - 'CONTRIBUTING.md' + # Scheduler consumers and the offline extractor's .sh/.func inputs. + - 'scripts/**/*.sh' + - 'scripts/**/*.func' + - 'lang/*.json' workflow_dispatch: permissions: @@ -39,6 +47,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: '22.14.0' + - name: Check scheduler test dependencies + run: | + test -x /bin/bash + test -x /usr/bin/jq - name: Run complete offline script test suite env: PYTHONDONTWRITEBYTECODE: '1' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78223628..179c63b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -826,7 +826,8 @@ From the repository root, run the complete script suite: python3 -m unittest discover -s .github/scripts/tests -v ``` -Prerequisites: Python 3.11+ (standard library only) and Node **22.14+** +Prerequisites: Python 3.11+ (Python standard library only), Node **22.14+**, +`/bin/bash`, and `/usr/bin/jq` for the isolated scheduler fixtures. (Node 22 LTS is the CI baseline). Node 20 can still build the Monitor, but cannot run this suite's native TypeScript consumer test. The test passes `--experimental-strip-types` explicitly and executes the actual pure lookup @@ -835,6 +836,17 @@ It does not launch React or require `npm install`, `pip install`, a Proxmox host API keys, or a translation provider. Generator tests use isolated temporary catalogs and fake/forbidden providers; repository catalogs are not regenerated. +Scheduler fixtures discover every shipped `lang/*.json` cache and run the actual +`translate` lookup at both empty-job consumers for PBS and local archive backends. +They accept English fallback for missing/empty entries; complete translations are +not required. Unconditional synthetic cases cover English bypass, missing files, +missing keys and translated messages/hints. Only bounded shell functions are +extracted: no administrative script is sourced or run in full. These scheduler +messages have no placeholders; named-token preservation is tested separately by +the existing cache-generator tests, not by scheduler/count-message assertions. +The workflow watches all `lang/*.json` catalogs plus `.sh` and `.func` inputs +under `scripts/`, and checks the Bash/jq prerequisites before running the suite. + If Node is not on `PATH`, select a local executable without a global install: ```bash