Skip to content
Merged
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
59 changes: 59 additions & 0 deletions apps/desktop/src/main/__tests__/icon-governance-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ const STROKE_EXCEPTION_FILES = new Set([
resolve(REPO_ROOT, 'apps/desktop/src/renderer/settings/provider-brand-marks.tsx'),
]);

// Governance: inline hand-drawn `<svg>` literals are an anti-pattern —
// generic UI glyphs must ride the @maka/ui/icons lucide funnel so they share
// one stroke, one sizing seam, and one family. The ONLY files allowed to hold
// a raw `<svg>` are the vendored *brand* marks (each audited below); they carry
// official multi-color logo geometry that no generic icon can stand in for:
// - packages/ui/src/bot-brand-logo.tsx — Telegram/Feishu/WeCom/… channel
// brand logos (the icon-set-no-direct-lucide contract pins each provider
// to a local SVG component, so this file MUST keep inline SVG).
// - apps/desktop/src/renderer/settings/provider-brand-marks.tsx — the LLM
// provider brand marks (SiliconCloud, Ollama, xAI, …); every path here is
// byte-provenance-pinned above to an upstream brand package.
// packages/ui/src/icons.tsx is NOT listed: it is a pure lucide re-export with
// zero `<svg>` literals, so it needs no exemption. Any NEW raw `<svg>` under
// the two source trees must fail this suite and move to @maka/ui/icons.
const INLINE_SVG_ALLOWLIST = new Set([
resolve(REPO_ROOT, 'packages/ui/src/bot-brand-logo.tsx'),
resolve(REPO_ROOT, 'apps/desktop/src/renderer/settings/provider-brand-marks.tsx'),
]);

async function walkTsx(dir: string): Promise<string[]> {
const out: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
Expand Down Expand Up @@ -128,6 +147,46 @@ describe('icon + typography governance contract', () => {
);
});

it('bans inline hand-drawn <svg> in .tsx outside the brand-mark allowlist', async () => {
// Machine-walk both source trees the same way the repo-wide
// OverlayScrollbars ban walks its workspaces — any .tsx that isn't a
// vendored brand mark must route generic glyphs through @maka/ui/icons
// instead of hand-drawing an <svg>.
const dirs = [
resolve(REPO_ROOT, 'apps/desktop/src/renderer'),
resolve(REPO_ROOT, 'packages/ui/src'),
];
const offenders: string[] = [];
const seenAllowlisted = new Set<string>();
for (const dir of dirs) {
for (const file of await walkTsx(dir)) {
const src = await readFile(file, 'utf8');
const hasInlineSvg = /<svg[\s/>]/.test(src);
if (INLINE_SVG_ALLOWLIST.has(file)) {
if (hasInlineSvg) seenAllowlisted.add(file);
continue;
}
if (hasInlineSvg) offenders.push(rel(file));
}
}
assert.deepEqual(
offenders,
[],
'Inline hand-drawn <svg> is an anti-pattern — replace it with a semantically-correct '
+ 'icon from @maka/ui/icons (add the re-export to packages/ui/src/icons.tsx if missing). '
+ `Offending files:\n ${offenders.join('\n ')}`,
);
// Guard the allowlist against rot: if a listed brand-mark file no longer
// holds inline SVG, drop it from INLINE_SVG_ALLOWLIST rather than leaving a
// stale exemption that would silently re-permit a future offender.
for (const file of INLINE_SVG_ALLOWLIST) {
assert.ok(
seenAllowlisted.has(file),
`${rel(file)} is allowlisted for inline SVG but no longer contains any — remove it from INLINE_SVG_ALLOWLIST`,
);
}
});

it('session-sidebar-nav imports exactly the decided semantic icon set from ./icons.js', async () => {
const src = await readFile(SIDEBAR_NAV_FILE, 'utf8');
const importMatch = src.match(/import\s*\{([^}]*)\}\s*from\s*'\.\/icons\.js'/);
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop/src/main/__tests__/visual-smoke-fixture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,20 +627,53 @@ describe('visual smoke fixture mode', () => {
}
});

it('module fixtures open Skills and Daily Review in the app module surface', async () => {
it('module fixtures open Skills, MCP and Daily Review in the app module surface', async () => {
const skills = resolveVisualSmokeFixture('module-skills', false);
const mcp = resolveVisualSmokeFixture('module-mcp', false);
const dailyReview = resolveVisualSmokeFixture('module-daily-review', false);

assert.ok(skills);
assert.ok(mcp);
assert.ok(dailyReview);
assert.equal(getVisualSmokeState(skills)?.sidebarSection, 'skills');
assert.equal(getVisualSmokeState(skills)?.sidebarCollapsed, false);
assert.equal(getVisualSmokeState(skills)?.activeSessionId, 'visual-smoke-turn');
assert.equal(getVisualSmokeState(mcp)?.sidebarSection, 'mcp');
assert.equal(getVisualSmokeState(mcp)?.sidebarCollapsed, false);
assert.equal(getVisualSmokeState(mcp)?.activeSessionId, 'visual-smoke-turn');
assert.equal(getVisualSmokeState(dailyReview)?.sidebarSection, 'daily-review');
assert.equal(getVisualSmokeState(dailyReview)?.sidebarCollapsed, false);
assert.equal(getVisualSmokeState(dailyReview)?.activeSessionId, 'visual-smoke-turn');
});

it('module-mcp seeds a couple of installed servers so the 已安装 list renders', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-visual-smoke-mcp-'));
try {
const fixture = resolveVisualSmokeFixture('module-mcp', false);
assert.ok(fixture);
await seedVisualSmokeFixture({
workspaceRoot,
fixture,
credentialStore: fakeCredentialStore(),
now: 1_700_000_000_000,
});
const config = JSON.parse(await readFile(join(workspaceRoot, 'mcp.json'), 'utf8')) as {
version: number;
mcpServers: Record<string, { enabled?: boolean }>;
};
assert.equal(config.version, 1);
const ids = Object.keys(config.mcpServers);
assert.ok(ids.length >= 2, 'installed list needs >=2 servers to render meaningfully');
// enabled:false keeps visual-smoke deterministic — no real npx / HTTP
// connection is attempted, so the rows settle in the neutral 已停用 state.
for (const id of ids) {
assert.equal(config.mcpServers[id].enabled, false, `${id} stays disabled in the fixture`);
}
} finally {
await rm(workspaceRoot, { recursive: true, force: true });
}
});

it('module-skills seeds a managed-source market catalog (>=6 entries with categories) plus workspace skills', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-visual-smoke-skills-'));
const previousSourcesRoot = process.env.MAKA_SKILL_SOURCES_ROOT;
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/src/main/visual-smoke-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const VISUAL_SMOKE_SCENARIOS = new Set<VisualSmokeScenario>([
'settings-usage',
'settings-health',
'module-skills',
// MCP module page (SVG-governance + polish campaign): opens the 扩展 → MCP
// surface with a seeded mcp.json so the market grid, tab row, hero banner,
// and the installed server list all render for the alignment auditor + CDP
// capture in light / dark.
'module-mcp',
'module-daily-review',
// PR109b: workstation-statuses — seed one session per SessionStatus
// (running / waiting_for_user / blocked × 4 reasons / active / review
Expand Down Expand Up @@ -418,6 +423,10 @@ export function getVisualSmokeState(fixture: VisualSmokeFixture | null): VisualS
return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'health' };
case 'module-skills':
return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'skills', sidebarCollapsed: false };
case 'module-mcp':
// Open the 扩展 → MCP module directly so the CDP baseline captures the
// real market grid, tab row, hero banner, and installed server list.
return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'mcp', sidebarCollapsed: false };
case 'module-daily-review':
return { ...state, activeSessionId: TURN_SESSION_ID, sidebarSection: 'daily-review', sidebarCollapsed: false };
case 'workstation-statuses':
Expand Down Expand Up @@ -603,6 +612,37 @@ export async function seedVisualSmokeFixture(input: {
if (input.fixture.scenario === 'module-skills') {
await seedSkillsMarketFixture(input.workspaceRoot);
}
if (input.fixture.scenario === 'module-mcp') {
await seedMcpFixture(input.workspaceRoot);
}
}

/**
* MCP module fixture: seeds an mcp.json with a couple of installed servers so
* the 已安装 tab and the server rows render for the alignment auditor + CDP
* capture. Both are `enabled: false` so no real `npx` / HTTP connection is
* attempted in visual-smoke mode — the rows render deterministically in the
* neutral 已停用 state (exception-only status: no color unless a real failure).
* The 市场 tab is the default surface and is driven by the static MCP_CATALOG,
* so it renders without any on-disk seed.
*/
async function seedMcpFixture(workspaceRoot: string): Promise<void> {
const config = {
version: 1,
mcpServers: {
filesystem: {
enabled: false,
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/workspace/maka'],
},
'linear-remote': {
enabled: false,
url: 'https://mcp.linear.app/sse',
transport: 'sse',
},
},
};
await writeJson(join(workspaceRoot, 'mcp.json'), config);
}

/**
Expand Down
Loading
Loading