Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/renderer/src/i18n/plugin-chrome-allowlist.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<string, number> = {}
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
})
})
})
39 changes: 33 additions & 6 deletions src/shared/plugins/plugin-language-pack-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>((node, part) => ({ [part]: node }), value)
}

describe('plugin language-pack artifacts', () => {
it('maps qualified IDs to distinct alphanumeric i18next resource languages', () => {
Expand Down Expand Up @@ -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
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

// 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
Expand Down
37 changes: 36 additions & 1 deletion src/shared/plugins/plugin-translatable-chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down