Compare commits

...
Author SHA1 Message Date
陈奇 91b8927c56 fix(plugin-themes): pin utility color aliases in security surfaces 2026-10-26 13:07:03 +00:00
3 changed files with 102 additions and 0 deletions

No files matched your search

@@ -0,0 +1,58 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { JSDOM } from 'jsdom';
import { compile } from '@tailwindcss/node';
import postcss from 'postcss';
import HostDetailsPanel from './HostDetailsPanel';
import { Card } from './ui/card';
import { TooltipProvider } from './ui/tooltip';
import { PluginPermissionPanel } from './settings/tabs/PluginPermissionPanel';
import { I18nProvider } from '../application/i18n/I18nProvider';
test('real Host form cards, inputs and Save consume scoped security aliases without freezing an ordinary card', async () => {
const markup = renderToStaticMarkup(<I18nProvider locale="en"><TooltipProvider>
<HostDetailsPanel availableKeys={[]} identities={[]} groups={[]} terminalThemeId="default" terminalFontSize={14} onSave={() => assert.fail('test never saves a host')} onCancel={() => {}} />
<PluginPermissionPanel pluginId="com.example.synthetic" onChanged={async () => {}} />
<Card id="ordinary-card">Outside the security surface</Card>
</TooltipProvider></I18nProvider>);
const dom = new JSDOM(`<!doctype html><html data-plugin-theme-active><body>${markup}</body></html>`);
try {
const panel = dom.window.document.querySelector('[data-section="host-details-panel"]')!;
assert.ok(panel, 'exercise the actual HostDetailsPanel and AsidePanel');
assert.ok(panel.querySelector('.bg-card'), 'General/Address/Credentials use real Card components');
assert.ok(panel.querySelector('input.bg-background'), 'exercise actual Input utilities');
assert.ok(panel.querySelector('button.bg-primary'), 'exercise the actual Save button');
const permissions = dom.window.document.querySelector('[data-host-security-surface]')!;
assert.ok(permissions.querySelector('[role="status"]'), 'exercise the real grant-panel security root without loading or changing any grants');
const source = await readFile(new URL('../index.css', import.meta.url), 'utf8');
const compiler = await compile(source, {base: path.resolve('.'), onDependency() {}});
const classes = [...new Set([...dom.window.document.querySelectorAll('[class]')].flatMap(el => [...el.classList]))];
const css = compiler.build(classes);
const parsed = postcss.parse(css);
const rules: postcss.Rule[] = [];
parsed.walkRules(rule => { rules.push(rule); });
const rule = rules.find(node => node.selector.startsWith(':root[data-plugin-theme-active]'));
assert.ok(rule && rule.type === 'rule');
const matches = [...dom.window.document.querySelectorAll(rule.selector)];
assert.ok(matches.includes(panel));
assert.ok(matches.includes(permissions));
assert.ok(!matches.includes(dom.window.document.getElementById('ordinary-card')!), 'security selector stays within the existing marked surfaces');
const declarations = new Map(rule.nodes.filter(node => node.type === 'decl').map(node => [node.prop, node]));
for (const name of ['background','foreground','card','card-foreground','popover','popover-foreground','primary','primary-foreground','secondary','secondary-foreground','muted','muted-foreground','accent','accent-foreground','destructive','destructive-foreground','border','input','ring']) {
const declaration = declarations.get(`--color-${name}`);
assert.equal(declaration?.value, `hsl(var(--host-security-${name}))`, `Tailwind alias ${name} cannot inherit the plugin value`);
assert.equal(declaration.important, true);
}
for (const name of ['card','background','primary']) {
const utility = rules.find(node => node.selector === `.bg-${name}`);
assert.ok(utility && utility.type === 'rule');
assert.ok(utility.nodes.some(node => node.type === 'decl' && node.prop === 'background-color' && node.value === `var(--color-${name})`), 'verify what the actual compiled utility consumes');
}
const mutedText = rules.find(node => node.selector === '.text-muted-foreground');
assert.ok(mutedText?.nodes.some(node => node.type === 'decl' && node.prop === 'color' && node.value === 'var(--color-muted-foreground)'));
} finally { dom.window.close(); }
});
+24
View File
@@ -133,3 +133,27 @@ This is source identity and a source-based ordering diagnosis, not a claim that
baseline runtime failure was reproduced locally. The updated test needs the new
exact-head CI result. That same first run passed the added Vite browser-import
smoke, resolving the earlier local temp-directory limitation for that head.
### Native security-color delta
Parent native acceptance at f868 verified zero-permission installation/enabling,
light/dark Preview and Cancel, Apply across main/Settings windows, Restore
default, restart persistence, and Disable fallback with the unavailable notice.
The isolated schema-3 profile was not touched. These core results remain valid.
Native QA also found Forest-colored inner New Host cards/inputs/Save and some
grant-panel text inside an otherwise fixed security container. The cause was
Tailwind's derived `--color-*` aliases: changing only raw semantic tokens at the
boundary does not recompute aliases already resolved on an ancestor. The same
existing security rule now fixes both raw tokens and those color aliases. The
selector and protected regions are unchanged; ordinary application cards remain
theme-controlled. No host, credential or grant operation changed.
The regression renders the actual HostDetailsPanel (including General, Address,
Credentials cards, real Inputs and Save), plus the real grant-panel root. It
compiles the production Tailwind source, verifies which aliases those utilities
consume, checks the fixed declarations, and verifies an ordinary card is outside
the security selector. This is component/compiled-CSS coverage, not a claim of
native computed-color acceptance. The parent's native color delta remains the
final visual check. A local Chromium probe aborted because its SUID sandbox was
not configured; sandboxing was not disabled to retry.
+20
View File
@@ -2221,6 +2221,26 @@ html[data-theme-transition] *::after {
--border: var(--host-security-border) !important;
--input: var(--host-security-input) !important;
--ring: var(--host-security-ring) !important;
/* Tailwind theme aliases resolve where declared; pin them in this scope too. */
--color-background: hsl(var(--host-security-background)) !important;
--color-foreground: hsl(var(--host-security-foreground)) !important;
--color-card: hsl(var(--host-security-card)) !important;
--color-card-foreground: hsl(var(--host-security-card-foreground)) !important;
--color-popover: hsl(var(--host-security-popover)) !important;
--color-popover-foreground: hsl(var(--host-security-popover-foreground)) !important;
--color-primary: hsl(var(--host-security-primary)) !important;
--color-primary-foreground: hsl(var(--host-security-primary-foreground)) !important;
--color-secondary: hsl(var(--host-security-secondary)) !important;
--color-secondary-foreground: hsl(var(--host-security-secondary-foreground)) !important;
--color-muted: hsl(var(--host-security-muted)) !important;
--color-muted-foreground: hsl(var(--host-security-muted-foreground)) !important;
--color-accent: hsl(var(--host-security-accent)) !important;
--color-accent-foreground: hsl(var(--host-security-accent-foreground)) !important;
--color-destructive: hsl(var(--host-security-destructive)) !important;
--color-destructive-foreground: hsl(var(--host-security-destructive-foreground)) !important;
--color-border: hsl(var(--host-security-border)) !important;
--color-input: hsl(var(--host-security-input)) !important;
--color-ring: hsl(var(--host-security-ring)) !important;
color: hsl(var(--foreground)) !important;
background-color: hsl(var(--background)) !important;
border-color: hsl(var(--border)) !important;