diff --git a/src/renderer/src/i18n/plugin-chrome-allowlist.test.ts b/src/renderer/src/i18n/plugin-chrome-allowlist.test.ts index 19859369a782..058a6530edc9 100644 --- a/src/renderer/src/i18n/plugin-chrome-allowlist.test.ts +++ b/src/renderer/src/i18n/plugin-chrome-allowlist.test.ts @@ -1,8 +1,9 @@ /** * The plugin-chrome exemption is a list of exact catalog paths, so it can rot in - * two directions: a renamed key leaves a dead entry behind, and a key that moves - * into a component the security tests name would widen the exemption silently. - * These assertions catch both. + * three directions: a renamed key leaves a dead entry behind, a key that moves + * into a component the security tests name would widen the exemption silently, + * and a path that drops out of the list takes every list-driven case with it. + * These assertions catch all three. */ import { describe, expect, it } from 'vitest' import { translatablePluginChromePaths } from '../../../shared/plugins/plugin-translatable-chrome' @@ -53,4 +54,24 @@ describe('translatable plugin chrome', () => { it('exempts no failure copy', () => { expect(translatablePluginChromePaths().filter((path) => path.endsWith('Failed'))).toEqual([]) }) + + // Why: every other case is driven off the list, so a shrinking list shrinks + // its own tests and still passes. A count per component is the one check the + // list cannot satisfy that way, and it does not keep a second copy of it. + it('exempts a fixed number of paths in each component', () => { + const counts: Record = {} + for (const path of translatablePluginChromePaths()) { + const component = path.slice('auto.components.settings.'.length).split('.')[0] + counts[component] = (counts[component] ?? 0) + 1 + } + + expect(counts).toEqual({ + PluginsSettingsSection: 9, + PluginMarketplaceBrowser: 14, + PluginDevelopmentSection: 6, + plugins: 6, + PluginInstallDialog: 13, + PluginSettingsRow: 9 + }) + }) }) diff --git a/src/shared/plugins/plugin-language-pack-artifact.test.ts b/src/shared/plugins/plugin-language-pack-artifact.test.ts index 1c58189845ca..4e2a41eec811 100644 --- a/src/shared/plugins/plugin-language-pack-artifact.test.ts +++ b/src/shared/plugins/plugin-language-pack-artifact.test.ts @@ -8,6 +8,12 @@ import { checkPluginLanguagePackCatalog, pluginLanguageResourceId } from './plugin-language-pack-artifact' +import { translatablePluginChromePaths } from './plugin-translatable-chrome' + +/** Nests a dotted path so the parser walks every container above the exempt leaf. */ +function catalogFor(path: string, value: string): unknown { + return path.split('.').reduceRight((node, part) => ({ [part]: node }), value) +} describe('plugin language-pack artifacts', () => { it('maps qualified IDs to distinct alphanumeric i18next resource languages', () => { @@ -61,18 +67,39 @@ describe('plugin language-pack artifacts', () => { ).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') }) }) + // Why: driven off the list itself rather than a sample, so a path added + // tomorrow is exercised the day it lands. Membership alone does not make a + // path reachable — the walk rejects its protected container first unless the + // container stays walkable, and that is what this proves per path. + it.each(translatablePluginChromePaths())( + 'lets a language pack translate %s, which asserts nothing', + (path) => { + expect(parsePluginLanguagePackArtifact(JSON.stringify(catalogFor(path, 'Перевод'))).ok).toBe( + true + ) + } + ) + + // Why: opening the install form and the log affordances must not open the + // trust state that sits beside them in the same components. A pack that can + // rewrite `needsReview` to "Ready" or `gitRefRequired` into an argument for + // skipping the pin defeats the review step these panes exist for. it.each([ - ['PluginsSettingsSection', 'title', 'Плагины'], - ['PluginMarketplaceBrowser', 'refresh', 'Обновить'], - ['PluginDevelopmentSection', 'title', 'Разработка'] - ])('lets a language pack translate %s.%s, which asserts nothing', (component, key, value) => { + ['PluginSettingsRow', 'needsReview', 'Проверено'], + ['PluginSettingsRow', 'invalid', 'В порядке'], + ['PluginSettingsRow', 'viewAdvisory', 'Подробности'], + ['PluginSettingsRow', 'blocked', 'Разрешён'], + ['PluginSettingsRow', 'bundled', 'Проверено Orca'], + ['PluginSettingsRow', 'dev', 'Официальный'], + ['PluginInstallDialog', 'gitRefRequired', 'Ref не обязателен, ставьте без него.'] + ])('still refuses %s.%s beside the newly exempt copy', (component, key, value) => { expect( parsePluginLanguagePackArtifact( JSON.stringify({ auto: { components: { settings: { [component]: { [key]: value } } } } }) - ).ok - ).toBe(true) + ) + ).toMatchObject({ ok: false, error: expect.stringContaining('protected security copy') }) }) // Why: the chrome exemption must not leak into the copy that carries a claim diff --git a/src/shared/plugins/plugin-translatable-chrome.ts b/src/shared/plugins/plugin-translatable-chrome.ts index 7a2e4c0fa2a7..5d76b7569a2c 100644 --- a/src/shared/plugins/plugin-translatable-chrome.ts +++ b/src/shared/plugins/plugin-translatable-chrome.ts @@ -62,7 +62,42 @@ const TRANSLATABLE_PLUGIN_CHROME = new Set([ 'auto.components.settings.plugins.search.install', 'auto.components.settings.plugins.search.permissions', 'auto.components.settings.plugins.search.logs', - 'auto.components.settings.plugins.search.development' + 'auto.components.settings.plugins.search.development', + + // Maturity badge on the pane heading. Says how finished the feature is, not + // what a plugin may do. + 'auto.components.settings.PluginsSettingsSection.experimental', + + // The install form: tab labels, field labels, placeholders, and the "you + // left this blank" validation. `gitRefRequired` stays protected — it argues + // why pinning a ref matters, which is a security claim. + 'auto.components.settings.PluginInstallDialog.cancel', + 'auto.components.settings.PluginInstallDialog.gitLabel', + 'auto.components.settings.PluginInstallDialog.gitPlaceholder', + 'auto.components.settings.PluginInstallDialog.gitTab', + 'auto.components.settings.PluginInstallDialog.gitUrlRequired', + 'auto.components.settings.PluginInstallDialog.install', + 'auto.components.settings.PluginInstallDialog.installing', + 'auto.components.settings.PluginInstallDialog.localLabel', + 'auto.components.settings.PluginInstallDialog.localPlaceholder', + 'auto.components.settings.PluginInstallDialog.localRequired', + 'auto.components.settings.PluginInstallDialog.localTab', + 'auto.components.settings.PluginInstallDialog.source', + 'auto.components.settings.PluginInstallDialog.title', + + // Log affordances and runtime state on an installed row. Trust state stays + // protected — `blocked`, `bundled`, `dev`, `needsReview`, `invalid`, + // `viewAdvisory` — as do the enable, remove and rollback actions: those are + // what the reader weighs when deciding whether this plugin should run. + 'auto.components.settings.PluginSettingsRow.hideLogs', + 'auto.components.settings.PluginSettingsRow.loadingLogs', + 'auto.components.settings.PluginSettingsRow.logCount', + 'auto.components.settings.PluginSettingsRow.moreActions', + 'auto.components.settings.PluginSettingsRow.noLogs', + 'auto.components.settings.PluginSettingsRow.restartCount', + 'auto.components.settings.PluginSettingsRow.restarting', + 'auto.components.settings.PluginSettingsRow.running', + 'auto.components.settings.PluginSettingsRow.viewLogs' ]) /** True when a protected-prefix path is plugin chrome a language pack may translate. */