test(01-12): wave-0 — scaffold RED unit tests (tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity)

Wave 0 of the design-integration plan. Six new test files at tests/build/
and tests/i18n/ pin the contracts that later waves will GREEN:

- tokens-adopted.test.ts (4 cases): src/shared/tokens.css exists +
  parses; src/popup/style.css @imports it; popup/style.css has zero
  hex literals; welcome.css conditional check.
- fonts-present.test.ts: 7 required WOFF2 faces (Lora normal + Plex Sans
  ×4 + Plex Mono ×2) + LICENSE-Lora + LICENSE-IBM-Plex + README +
  optional Lora-Italic (A5 verify-at-execute).
- icons-present.test.ts (15 cases across 3 sizes): existence, size FLOOR
  per assets-spec.md, PNG signature, dimensions, color-type byte === 6
  (RGBA — RED until Wave 2 rsvg-convert overwrites the 16-bit-RGB
  placeholders).
- no-remote-fonts.test.ts: production dist/ contains zero
  fonts.googleapis.com / https://fonts / googleapis substrings (MV3 CSP
  self-host invariant T-01-12-01).
- manifest-i18n.test.ts (10 cases): manifest:name === '__MSG_extName__',
  :description === '__MSG_extDesc__', :default_locale === 'en',
  :action.default_title === '__MSG_tooltipOff__'; _locales/{en,ru}/
  messages.json carry D-07 + D-08 canonical strings.
- locale-parity.test.ts (4 cases): ru→en parity, en→ru symmetric,
  non-empty .message strings (RESEARCH Pitfall 4 mitigation).

Current polarity: 29 RED + 18 GREEN across the 6 new files (placeholders
already clear dim+size floors; no-remote-fonts vacuous-GREEN since
tokens.css doesn't yet exist with remote URLs). Existing 100/100 vitest
baseline preserved (verified SKIP_BUILD=1 npx vitest run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 21:56:08 +02:00
parent 3fe018beb9
commit 34a9ce10d4
6 changed files with 609 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
// tests/i18n/locale-parity.test.ts — Plan 01-12 Wave 0 RED unit test.
//
// Enforces default_locale parity: every key in _locales/ru/messages.json
// must exist in _locales/en/messages.json (the default_locale fallback).
// Symmetric check for cleanliness; non-empty message strings throughout.
//
// Polarity at Wave 0 land: RED across the board (no _locales/ yet).
// Flips GREEN after Wave 3 Task 1 lands both locale files with parity.
//
// References:
// - RESEARCH Pitfall 4 (missing default_locale key returns empty string,
// not the key name — silent UI breakage if EN locale lacks a key the
// RU locale defines).
import { describe, expect, it } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
const EN_LOCALE_PATH = resolvePath(process.cwd(), '_locales/en/messages.json');
const RU_LOCALE_PATH = resolvePath(process.cwd(), '_locales/ru/messages.json');
interface I18nEntry {
message: string;
description?: string;
}
type LocaleFile = Record<string, I18nEntry>;
function loadLocale(filePath: string): LocaleFile {
if (!existsSync(filePath)) {
throw new Error(`Missing locale file: ${filePath} (RED until Wave 3 Task 1)`);
}
return JSON.parse(readFileSync(filePath, 'utf8'));
}
describe('Plan 01-12: _locales/ key parity (default_locale fallback invariant)', () => {
it('every key in _locales/ru/messages.json exists in _locales/en/messages.json', () => {
const ru = loadLocale(RU_LOCALE_PATH);
const en = loadLocale(EN_LOCALE_PATH);
const ruKeys = Object.keys(ru);
const missingInEn = ruKeys.filter((k) => !(k in en));
expect(
missingInEn,
missingInEn.length === 0
? 'unreachable'
: `Keys present in ru/ but missing in en/ (silent UI breakage risk):\n - ${missingInEn.join('\n - ')}`,
).toEqual([]);
});
it('every key in _locales/en/messages.json exists in _locales/ru/messages.json (symmetric)', () => {
const ru = loadLocale(RU_LOCALE_PATH);
const en = loadLocale(EN_LOCALE_PATH);
const enKeys = Object.keys(en);
const missingInRu = enKeys.filter((k) => !(k in ru));
expect(
missingInRu,
missingInRu.length === 0
? 'unreachable'
: `Keys present in en/ but missing in ru/ (cleanliness):\n - ${missingInRu.join('\n - ')}`,
).toEqual([]);
});
it('every key in _locales/en/messages.json has a non-empty .message string', () => {
const en = loadLocale(EN_LOCALE_PATH);
for (const k of Object.keys(en)) {
expect(typeof en[k].message, `en:${k}.message must be a string`).toBe('string');
expect(en[k].message.length, `en:${k}.message must be non-empty`).toBeGreaterThan(0);
}
});
it('every key in _locales/ru/messages.json has a non-empty .message string', () => {
const ru = loadLocale(RU_LOCALE_PATH);
for (const k of Object.keys(ru)) {
expect(typeof ru[k].message, `ru:${k}.message must be a string`).toBe('string');
expect(ru[k].message.length, `ru:${k}.message must be non-empty`).toBeGreaterThan(0);
}
});
});

View File

@@ -0,0 +1,87 @@
// tests/i18n/manifest-i18n.test.ts — Plan 01-12 Wave 0 RED unit test.
//
// Asserts manifest.json migrated to chrome i18n placeholders with
// default_locale='en' + _locales/{en,ru}/messages.json carrying the
// D-07 + D-08 canonical strings.
//
// Polarity at Wave 0 land: RED across the board (manifest.name still
// 'AI Call Recorder'; no default_locale; no _locales/). Flips GREEN
// after Wave 3 Task 1 migrates manifest + lands messages.json files.
//
// References:
// - RESEARCH §10 + §11 (Chrome i18n schema; __MSG_* placeholder rules)
// - brand-decisions-v1.md D-07 override (`Mokosh — Session Capture`)
// - brand-decisions-v1.md D-08 tagline (`Thirty seconds ago, always at hand.`)
import { describe, expect, it } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
const MANIFEST_PATH = resolvePath(process.cwd(), 'manifest.json');
const EN_LOCALE_PATH = resolvePath(process.cwd(), '_locales/en/messages.json');
const RU_LOCALE_PATH = resolvePath(process.cwd(), '_locales/ru/messages.json');
interface I18nEntry {
message: string;
description?: string;
}
type LocaleFile = Record<string, I18nEntry>;
describe('Plan 01-12: manifest i18n migration (__MSG_*__ + default_locale)', () => {
it('manifest.json:name === "__MSG_extName__" (i18n placeholder)', () => {
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
expect(manifest.name).toBe('__MSG_extName__');
});
it('manifest.json:description === "__MSG_extDesc__"', () => {
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
expect(manifest.description).toBe('__MSG_extDesc__');
});
it('manifest.json:default_locale === "en"', () => {
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
expect(manifest.default_locale).toBe('en');
});
it('manifest.json:action.default_title === "__MSG_tooltipOff__"', () => {
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
expect(manifest.action?.default_title).toBe('__MSG_tooltipOff__');
});
});
describe('Plan 01-12: _locales/en/messages.json (default_locale fallback)', () => {
it('_locales/en/messages.json exists + parses', () => {
expect(existsSync(EN_LOCALE_PATH), `Expected ${EN_LOCALE_PATH}`).toBe(true);
const en = JSON.parse(readFileSync(EN_LOCALE_PATH, 'utf8'));
expect(typeof en).toBe('object');
});
it('en:extName === "Mokosh — Session Capture" (D-07 override)', () => {
const en: LocaleFile = JSON.parse(readFileSync(EN_LOCALE_PATH, 'utf8'));
expect(en.extName?.message).toBe('Mokosh — Session Capture');
});
it('en:extDesc === "Thirty seconds ago, always at hand." (D-08 tagline)', () => {
const en: LocaleFile = JSON.parse(readFileSync(EN_LOCALE_PATH, 'utf8'));
expect(en.extDesc?.message).toBe('Thirty seconds ago, always at hand.');
});
});
describe('Plan 01-12: _locales/ru/messages.json (primary operator locale)', () => {
it('_locales/ru/messages.json exists + parses', () => {
expect(existsSync(RU_LOCALE_PATH), `Expected ${RU_LOCALE_PATH}`).toBe(true);
const ru = JSON.parse(readFileSync(RU_LOCALE_PATH, 'utf8'));
expect(typeof ru).toBe('object');
});
it('ru:extName === "Mokosh — Запись сессии"', () => {
const ru: LocaleFile = JSON.parse(readFileSync(RU_LOCALE_PATH, 'utf8'));
expect(ru.extName?.message).toBe('Mokosh — Запись сессии');
});
it('ru:extDesc === "Тридцать секунд назад, всегда под рукой."', () => {
const ru: LocaleFile = JSON.parse(readFileSync(RU_LOCALE_PATH, 'utf8'));
expect(ru.extDesc?.message).toBe('Тридцать секунд назад, всегда под рукой.');
});
});