From b69dac8f24880ccacb7d9b3e6298ec6ad63d4753 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 5 Oct 2026 02:08:08 +0800 Subject: [PATCH 1/5] fix(deps): bump http-cache-semantics to 4.3.0 (CVE-2026-93748) GHSA-ch52-4w7c-c8xp: http-cache-semantics <= 4.2.0 fails to validate security-zeroed cache entries when processing client max-stale directives, allowing cross-user cached-response disclosure in shared caches. 4.3.0 (npm latest, released 2026-10-04) restructures the max-stale evaluation path and is covered by its 128-test upstream suite. Impact-verified for this repo: the only consumer chain is electron-builder -> app-builder-lib -> @electron/get -> got -> cacheable-request (dev tooling). @electron/get 3.x downloads via native fetch, and got only routes through cacheable-request when a cache option is passed (nothing in this tree passes one), so the vulnerable code was never on any active request path. A/B request comparison shows byte-identical outgoing requests across both versions. --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04ae217f..f982c87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10023,9 +10023,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "dev": true, "license": "BSD-2-Clause" }, From f77e77cc71ea98c248e9228fa1e0c3dedda69225 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 5 Oct 2026 02:08:29 +0800 Subject: [PATCH 2/5] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=AE=A1=E8=AE=A1=E5=8F=91=E7=8E=B0=E7=9A=84=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E6=96=AD=E8=A8=80=E3=80=81=E7=8A=B6=E6=80=81=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=E4=B8=8E=E4=B8=8D=E7=A8=B3=E5=AE=9A=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对全部测试代码做了一轮审计(electron / server / src),修复 15 处问题: 失效或无效断言: - pluginManager: symlink 平台限制改用 t.skip,安全断言不再静默跳过 - pluginPageBridge: 边界用例改为恰好 160k 字符,真正覆盖 MAX_AI_USER_CHARS - xTweetAuthPersistence: migrate 前置条件缺失时测试显式失败(CWE-922 回归网) - webdavService: 删除从不传递的 AbortController 与恒真断言 - autoSync.repoHash: 删除与自身比较的恒真断言 - categoryLanguage: fixture 更新分支删除无意义断言 - abortUtils: 用 vi.getTimerCount() 断言定时器确被清理 - routes(mcp): 删除 typeof boolean 恒真用例,说明改为注释 不稳定与状态泄漏: - parity(mcp): beforeEach 复位 getVectorAvailability,消除用例顺序依赖 - autoSync.repoHash: two-pull describe 补 resetSyncHashes 与 store 状态恢复 - aiRequestLimiter: RPM 窗口放宽到 2s,探测 200ms,消除事件循环卡顿竞态 - ReleaseCard: 外层 beforeEach 复位模块级 store 语言,en 不再泄漏 - MarkdownRenderer: 数学门控改为直接断言导出的 MATH_PATTERN, 移除 50ms 真实定时器竞态与 cwd 相关的整文件文本扫描 - pluginMarketplace: chmod 000 用 try/finally 恢复;afterEach 清理临时目录 - RepositoryReleaseSheet: 修复注释中的损坏 UTF-8 字节 --- electron/plugins/pluginManager.test.js | 3 +-- electron/plugins/pluginMarketplace.test.js | 20 +++++++++++----- electron/plugins/pluginPageBridge.test.js | 7 +++--- server/tests/mcp/parity.test.ts | 8 ++++++- server/tests/mcp/routes.test.ts | 8 +++---- src/components/MarkdownRenderer.test.tsx | 23 +++++++++++-------- src/components/MarkdownRenderer.tsx | 5 ++-- src/components/ReleaseCard.test.tsx | 2 ++ .../RepositoryReleaseSheet.test.tsx | 10 ++++---- src/services/aiRequestLimiter.test.ts | 4 +++- src/services/autoSync.repoHash.test.ts | 9 ++++++-- src/services/webdavService.test.ts | 6 ++--- .../__fixtures__/categoryLanguage.test.ts | 1 - .../persistence/xTweetAuthPersistence.test.ts | 8 ++++--- src/utils/abortUtils.test.ts | 3 ++- 15 files changed, 71 insertions(+), 46 deletions(-) diff --git a/electron/plugins/pluginManager.test.js b/electron/plugins/pluginManager.test.js index ca46710f..298e3078 100644 --- a/electron/plugins/pluginManager.test.js +++ b/electron/plugins/pluginManager.test.js @@ -649,8 +649,7 @@ test('installs a validated local directory as disabled without copying symlinks' try { fs.symlinkSync(path.join(source, 'worker.js'), path.join(linked, 'extra-link.js'), 'file'); } catch (error) { - t.diagnostic(`Symlink creation unavailable: ${error.message}`); - return; + t.skip(`Symlink creation unavailable: ${error.message}`); } assert.equal(manager.installFromDirectory(linked).error.code, 'PLUGIN_PACKAGE_SYMLINK'); assert.equal(fs.existsSync(path.join(root, 'com.example.linked-install')), false); diff --git a/electron/plugins/pluginMarketplace.test.js b/electron/plugins/pluginMarketplace.test.js index f56da747..4b505536 100644 --- a/electron/plugins/pluginMarketplace.test.js +++ b/electron/plugins/pluginMarketplace.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { describe, it, beforeEach } = require('node:test'); +const { describe, it, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); @@ -161,6 +161,10 @@ describe('plugin marketplace', () => { stateFilePath = path.join(statePath, 'plugins-marketplace.json'); }); + afterEach(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + }); + function makeManager() { return createPluginManager({ pluginsRoot, statePath: path.join(tmpRoot, 'plugins-state.json') }); } @@ -759,11 +763,15 @@ describe('plugin marketplace', () => { // 临时弄坏 pluginsRoot 权限,让 installFromDirectory 的 mkdirSync 同步抛异常 const rootMode = fs.statSync(pluginsRoot).mode; fs.chmodSync(pluginsRoot, 0o000); - const second = await marketplace.install({ sourceId, directoryName: 'fixture', replace: true, - expectedPluginId: 'com.example.marketplace-fixture', - expectedVersion: '1.0.0', - }); - fs.chmodSync(pluginsRoot, rootMode); + let second; + try { + second = await marketplace.install({ sourceId, directoryName: 'fixture', replace: true, + expectedPluginId: 'com.example.marketplace-fixture', + expectedVersion: '1.0.0', + }); + } finally { + fs.chmodSync(pluginsRoot, rootMode); + } assert.equal(second.success, false, '同步异常必须被外层 catch 兜住'); const listed = await pluginManager.list(); diff --git a/electron/plugins/pluginPageBridge.test.js b/electron/plugins/pluginPageBridge.test.js index e79f5048..1d7a4cd4 100644 --- a/electron/plugins/pluginPageBridge.test.js +++ b/electron/plugins/pluginPageBridge.test.js @@ -26,12 +26,13 @@ test('AI page requests require bounded prompts and cannot carry credentials', () pluginId: 'com.example.page', pageId: 'dashboard', capability: 'ai', operation: 'generate', args: { system: 'Summarize', user: 'Example repository', maxTokens: 500 }, }); - // 内容生成页会把 README 全文放进正文:160k 字符的正文要能通过, + // 内容生成页会把 README 全文放进正文:恰好 160k 字符的正文要能通过(20 前缀 + 31_996*5), // 同时仍在 1 MiB 通用字节预算之内。 - const readmeSizedPrompt = 'README (full text):\n' + '仓库说明 '.repeat(20_000); + const readmeSizedPrompt = 'README (full text):\n' + '仓库说明 '.repeat(31_996); + assert.equal(readmeSizedPrompt.length, 160_000); assert.doesNotThrow(() => validatePageCapabilityRequest({ pluginId: 'com.example.page', pageId: 'dashboard', method: 'ai.generate', - args: { system: 'Summarize', user: readmeSizedPrompt.slice(0, 160_000) }, + args: { system: 'Summarize', user: readmeSizedPrompt }, })); for (const args of [ { system: '', user: '' }, diff --git a/server/tests/mcp/parity.test.ts b/server/tests/mcp/parity.test.ts index e1fd7552..6adfe915 100644 --- a/server/tests/mcp/parity.test.ts +++ b/server/tests/mcp/parity.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; const providerMocks = vi.hoisted(() => ({ getRepository: vi.fn(), @@ -44,6 +44,12 @@ function contract(tool: { name: string; inputSchema?: Record }) } describe('backend/Electron MCP parity', () => { + // registerMcpTools 在注册时读取 getVectorAvailability(), + // 必须在每个用例前复位,避免上一个用例的覆写泄漏到后续注册。 + beforeEach(() => { + providerMocks.getVectorAvailability.mockReturnValue({ available: true }); + }); + it('keeps the same names and input property sets for vector-enabled MCP', () => { const backend = getBackendTools().map(contract); const electron = getElectronTools(true).map((tool: { name: string; inputSchema?: { properties?: Record } }) => ({ diff --git a/server/tests/mcp/routes.test.ts b/server/tests/mcp/routes.test.ts index 367f5b98..6cfa5154 100644 --- a/server/tests/mcp/routes.test.ts +++ b/server/tests/mcp/routes.test.ts @@ -21,6 +21,9 @@ async function canOpenSqlite(): Promise { } const dbAvailable = await canOpenSqlite(); +// DB-backed suites skip instead of failing when the native sqlite binding cannot +// be opened (e.g. Node 26 without a rebuildable better-sqlite3); the pure-unit +// suites below always run. const describeIfDb = dbAvailable ? describe : describe.skip; describeIfDb('MCP admin + transport auth', () => { @@ -163,9 +166,4 @@ describe('MCP pure units always run', () => { expect(timingSafeEqualString(t, t)).toBe(true); expect(timingSafeEqualString(t, t + 'x')).toBe(false); }); - - it('notes when sqlite native binding unavailable', () => { - // Environment limitation (e.g. Node 26 without rebuildable better-sqlite3) - expect(typeof dbAvailable).toBe('boolean'); - }); }); diff --git a/src/components/MarkdownRenderer.test.tsx b/src/components/MarkdownRenderer.test.tsx index 58e99501..ee322d3f 100644 --- a/src/components/MarkdownRenderer.test.tsx +++ b/src/components/MarkdownRenderer.test.tsx @@ -1,7 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; -import { readFileSync } from 'node:fs'; -import MarkdownRenderer from '../components/MarkdownRenderer'; +import MarkdownRenderer, { MATH_PATTERN } from '../components/MarkdownRenderer'; vi.mock('../store/useAppStore', () => ({ useAppStore: vi.fn((selector) => { @@ -692,9 +691,14 @@ describe('MarkdownRenderer', () => { describe('Math (KaTeX)', () => { it('uses a Safari-compatible inline math detector', () => { - const source = readFileSync('src/components/MarkdownRenderer.tsx', 'utf8'); - - expect(source).not.toContain('(? { @@ -704,11 +708,10 @@ describe('MarkdownRenderer', () => { }, { timeout: 5000 }); }, 10000); - it('should not load math support for plain documents', async () => { - const { container } = render(); - // Give the effect a tick; no katex nodes should ever appear - await new Promise((resolve) => setTimeout(resolve, 50)); - expect(container.querySelector('.katex')).toBeNull(); + it('should not load math support for plain documents', () => { + // KaTeX 是否加载完全由 MATH_PATTERN.test(content) 门控(与 effect 一致), + // 直接断言门控为 false,不依赖真实定时器长度的竞态。 + expect(MATH_PATTERN.test('Just $5 and text')).toBe(false); }); }); }); diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 7c623bb5..6617ec98 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -49,8 +49,9 @@ const BASE_REMARK_PLUGINS = [remarkGfm, remarkAlert, remarkGemoji]; const REHYPE_PLUGINS_NO_HTML: never[] = []; // Matches $$display$$, \(inline\), \[display\] and $inline$ math so KaTeX is -// only loaded for documents that actually use it. -const MATH_PATTERN = +// only loaded for documents that actually use it. Exported so tests can assert +// the lazy-load gate directly (and keep the regex lookbehind-free for Safari). +export const MATH_PATTERN = /\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\)|\$(?!\s)(?:\\.|[^$\\\n])*?[^\s$\\\n]\$/; interface MathPlugins { diff --git a/src/components/ReleaseCard.test.tsx b/src/components/ReleaseCard.test.tsx index 72e9c049..a1b07222 100644 --- a/src/components/ReleaseCard.test.tsx +++ b/src/components/ReleaseCard.test.tsx @@ -84,6 +84,8 @@ const renderCard = (props: Partial[0]> = {}) => { describe('ReleaseCard asset updated indicator', () => { beforeEach(() => { vi.clearAllMocks(); + // 模块级 store 状态被所有用例共享:英文用例设置的 en 不得泄漏到后续用例 + storeState.language = 'zh'; }); it('shows container-level and per-asset indicators from the same source (updated_asset_ids)', () => { diff --git a/src/components/RepositoryReleaseSheet.test.tsx b/src/components/RepositoryReleaseSheet.test.tsx index 0e67e013..e850436c 100644 --- a/src/components/RepositoryReleaseSheet.test.tsx +++ b/src/components/RepositoryReleaseSheet.test.tsx @@ -89,8 +89,8 @@ const renderSheet = () => render( /> ); -// 侧栏现在还会渲染 Repository Health 事实面板,面板里的「最新稳定版本」同样是 tag 名ã€? -// 因此针对 Release 条目的查询必须限定在 Release 列表容器内,避免与事实面板串台ã€? +// 侧栏现在还会渲染 Repository Health 事实面板,面板里的「最新稳定版本」同样是 tag 名, +// 因此针对 Release 条目的查询必须限定在 Release 列表容器内,避免与事实面板串台。 const releaseList = () => within(screen.getByTestId('release-list')); describe('RepositoryReleaseSheet', () => { @@ -159,15 +159,15 @@ describe('RepositoryReleaseSheet', () => { await user.click(releaseList().getByText('v1').closest('button')!); - // 可识别平台的资产渲染品牌徽章(与 ReleaseCard çš?AssetLeadingIcon 一致)ã€? - // getAllByTitle:simple-icons çš?svg 内部也带 ,需按徽ç«?class 过滤出外å±?spanã€? + // 可识别平台的资产渲染品牌徽章(与 ReleaseCard 的 AssetLeadingIcon 一致)。 + // getAllByTitle:simple-icons 的 svg 内部也带 <title>,需按徽章 class 过滤出外层 span。 const getBadge = (title: string) => screen.getAllByTitle(title).find((el) => el.classList.contains('asset-platform-badge')); expect(getBadge('macOS')).toBeDefined(); expect(getBadge('Windows')).toBeDefined(); expect(getBadge('Linux')).toBeDefined(); - // 平台不可识别的资产回退到通用下载图标,不猜平å�? + // 平台不可识别的资产回退到通用下载图标,不猜平台。 const zipRow = screen.getByText('myapp-1.0.zip').closest('tr'); expect(zipRow).not.toBeNull(); expect(zipRow!.querySelector('.asset-platform-badge')).toBeNull(); diff --git a/src/services/aiRequestLimiter.test.ts b/src/services/aiRequestLimiter.test.ts index c72a7fdb..4d47aa53 100644 --- a/src/services/aiRequestLimiter.test.ts +++ b/src/services/aiRequestLimiter.test.ts @@ -62,7 +62,9 @@ describe('AIRateLimiter', () => { }); it('requestsPerMinute 下并发 acquire 不超发', async () => { - const limiter = new AIRateLimiter({ requestsPerMinute: 2, rpmWindowMs: 500 }); + // 窗口取 2s、探测取 200ms:真实计时器下若 CI 事件循环短暂卡顿, + // 探测窗仍远小于限流窗口,避免过期定时器晚触发导致的偶发超发。 + const limiter = new AIRateLimiter({ requestsPerMinute: 2, rpmWindowMs: 2000 }); let acquired = 0; const pending = Array.from({ length: 4 }, async () => { const release = await limiter.acquire(); diff --git a/src/services/autoSync.repoHash.test.ts b/src/services/autoSync.repoHash.test.ts index a9fe8e57..20d5c91f 100644 --- a/src/services/autoSync.repoHash.test.ts +++ b/src/services/autoSync.repoHash.test.ts @@ -89,8 +89,6 @@ describe('backend sync hash convergence (Issue #304 loop-breaker)', () => { // The fix commits the raw backend hash (firstPullHash), not quickHash(merged). expect(firstPullHash).not.toBe(JSON.stringify(merged)); - // Second poll re-hashes the same backend payload → equals the committed hash. - expect(JSON.stringify(backendPayload)).toBe(firstPullHash); }); it('merged output is stable across repeated merges of the same inputs', () => { @@ -179,8 +177,10 @@ it('omits client-only fields from both pull and successful-push hashes', () => { describe('syncFromBackend two-pull loop (Issue #304 end-to-end)', () => { const backendPayload = [createRepository(1, { ai_summary: 'from backend' })]; + let originalState: ReturnType<typeof useAppStore.getState>; beforeEach(() => { + originalState = useAppStore.getState(); vi.mocked(backend.fetchRepositories).mockResolvedValue({ repositories: backendPayload, total: 1 }); vi.mocked(backend.fetchReleases).mockResolvedValue({ releases: [], total: 0 }); vi.mocked(backend.fetchAIConfigs).mockResolvedValue([]); @@ -195,6 +195,11 @@ describe('syncFromBackend two-pull loop (Issue #304 end-to-end)', () => { readmeMaxChars: 6000, }); vi.mocked(backend.fetchSettings).mockResolvedValue({}); + resetSyncHashes(); + }); + + afterEach(() => { + useAppStore.setState(originalState); }); it('applies a changed backend payload once; an unchanged second pull is a no-op', async () => { diff --git a/src/services/webdavService.test.ts b/src/services/webdavService.test.ts index 21bb98e7..78bde37a 100644 --- a/src/services/webdavService.test.ts +++ b/src/services/webdavService.test.ts @@ -63,18 +63,16 @@ describe('WebDAVService 传输层选择', () => { expect(headers?.Authorization).toBeUndefined(); }); - it('把调用方的 AbortSignal 与 timeoutMs 透传给后端代理', async () => { + it('把内部 AbortSignal 与 timeoutMs 透传给后端代理', async () => { vi.mocked(backend).isAvailable = true; proxyWebDAV.mockResolvedValue(new Response('{}', { status: 200 })); - const controller = new AbortController(); await davService().fileExists('data.json'); - // fileExists 内部自建 controller;这里只断言 timeoutMs 被显式传递 + // fileExists 内部自建 controller;这里断言 signal 与 timeoutMs 被显式传递 const call = proxyWebDAV.mock.calls[0]; expect(call[6]).toBeInstanceOf(AbortSignal); expect(call[7]).toBe(10000); - expect(controller.signal.aborted).toBe(false); }); it('上传时透传按体积计算出的动态超时', async () => { diff --git a/src/store/__fixtures__/categoryLanguage.test.ts b/src/store/__fixtures__/categoryLanguage.test.ts index 3a3db91e..702ae493 100644 --- a/src/store/__fixtures__/categoryLanguage.test.ts +++ b/src/store/__fixtures__/categoryLanguage.test.ts @@ -102,7 +102,6 @@ describe('内置分类语言行为 fixture(zh/en 基线)', () => { const snapshot = buildSnapshot(); if (shouldUpdateFixtures()) { writeFixture(FIXTURE_FILE, snapshot); - expect(snapshot).not.toBeNull(); return; } expect(snapshot).toEqual(readFixture(FIXTURE_FILE)); diff --git a/src/store/persistence/xTweetAuthPersistence.test.ts b/src/store/persistence/xTweetAuthPersistence.test.ts index a4ae0fc4..001eebd0 100644 --- a/src/store/persistence/xTweetAuthPersistence.test.ts +++ b/src/store/persistence/xTweetAuthPersistence.test.ts @@ -58,9 +58,11 @@ describe('xTweetAuth persistence & security', () => { }, }; - if (typeof appPersistenceOptions.migrate === 'function') { - const migrated = appPersistenceOptions.migrate(rawState as unknown, 14) as Record<string, unknown>; - expect('xTweetAuth' in migrated).toBe(false); + // 显式断言前置条件:migrate 缺失时测试必须失败,而不是静默跳过校验 + if (typeof appPersistenceOptions.migrate !== 'function') { + throw new Error('appPersistenceOptions.migrate 必须存在,否则遗留明文 xTweetAuth 快照无法被迁移清理'); } + const migrated = appPersistenceOptions.migrate(rawState as unknown, 14) as Record<string, unknown>; + expect('xTweetAuth' in migrated).toBe(false); }); }); diff --git a/src/utils/abortUtils.test.ts b/src/utils/abortUtils.test.ts index 94e58a2f..8c099560 100644 --- a/src/utils/abortUtils.test.ts +++ b/src/utils/abortUtils.test.ts @@ -47,7 +47,8 @@ describe('createCombinedAbortController', () => { const controller = createCombinedAbortController(undefined, 5_000); controller.abort(); - // 定时器已被清理:继续推进时间不应再触发任何 abort 副作用 + // 定时器已被清理:挂起定时器计数归零,推进时间也不应再触发任何 abort 副作用 + expect(vi.getTimerCount()).toBe(0); vi.advanceTimersByTime(10_000); expect(controller.signal.aborted).toBe(true); }); From 41ed43aed1d57b42a29912f603d4ccd0b1ba92bc Mon Sep 17 00:00:00 2001 From: AmintaCCCP <libogege@sina.com> Date: Mon, 5 Oct 2026 02:18:15 +0800 Subject: [PATCH 3/5] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=20CodeRabbit=20?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E6=84=8F=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pluginManager: t.skip 后显式 return(node:test 的 skip 不终止回调, 否则后续断言仍会执行) - MarkdownRenderer: lookbehind 检查同时拒绝正向 (?<= 与负向 (?<!, 🤖 Generated with [Z.ai Code](https://chat.z.ai) --- electron/plugins/pluginManager.test.js | 2 ++ src/components/MarkdownRenderer.test.tsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/electron/plugins/pluginManager.test.js b/electron/plugins/pluginManager.test.js index 298e3078..175aaded 100644 --- a/electron/plugins/pluginManager.test.js +++ b/electron/plugins/pluginManager.test.js @@ -650,6 +650,8 @@ test('installs a validated local directory as disabled without copying symlinks' fs.symlinkSync(path.join(source, 'worker.js'), path.join(linked, 'extra-link.js'), 'file'); } catch (error) { t.skip(`Symlink creation unavailable: ${error.message}`); + // node:test 的 t.skip 只标记跳过、不会终止回调,必须显式 return + return; } assert.equal(manager.installFromDirectory(linked).error.code, 'PLUGIN_PACKAGE_SYMLINK'); assert.equal(fs.existsSync(path.join(root, 'com.example.linked-install')), false); diff --git a/src/components/MarkdownRenderer.test.tsx b/src/components/MarkdownRenderer.test.tsx index ee322d3f..5063b0f6 100644 --- a/src/components/MarkdownRenderer.test.tsx +++ b/src/components/MarkdownRenderer.test.tsx @@ -693,7 +693,8 @@ describe('MarkdownRenderer', () => { it('uses a Safari-compatible inline math detector', () => { // 老 Safari 不支持 lookbehind:直接断言导出正则的 source, // 避免对整个文件做文本扫描,也去掉对 cwd 的依赖。 - expect(MATH_PATTERN.source).not.toContain('?<!'); + // 正向 (?<= 与负向 (?<! 两种 lookbehind 一并拒绝。 + expect(MATH_PATTERN.source).not.toMatch(/\(\?<([=!])/); // 防止为绕过检查而破坏检测能力:四种数学语法仍必须命中 expect(MATH_PATTERN.test('$$E=mc^2$$')).toBe(true); expect(MATH_PATTERN.test('\\[display\\]')).toBe(true); From 130d90ea9d02609a1561cd0c47f00190185fd0db Mon Sep 17 00:00:00 2001 From: AmintaCCCP <libogege@sina.com> Date: Mon, 5 Oct 2026 02:26:18 +0800 Subject: [PATCH 4/5] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=20pluginRuntime?= =?UTF-8?q?=20=E8=B0=83=E7=94=A8=E8=B6=85=E6=97=B6=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E7=9A=84=20CI=20=E5=90=AF=E5=8A=A8=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit timeoutMs 同时约束 Worker 启动与调用超时,真实 Worker 在高负载 CI 上 启动可能超过 50ms,导致 activate() 阶段直接抛 PLUGIN_RUNTIME_TIMEOUT。 提升到 2000ms 与同文件其他用例一致,被测的调用超时行为不变。 🤖 Generated with [Z.ai Code](https://chat.z.ai) --- electron/plugins/pluginRuntime.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electron/plugins/pluginRuntime.test.js b/electron/plugins/pluginRuntime.test.js index c26a6a38..d684c88c 100644 --- a/electron/plugins/pluginRuntime.test.js +++ b/electron/plugins/pluginRuntime.test.js @@ -75,7 +75,10 @@ test('terminates a plugin runtime when a call times out', async (t) => { entryPath, pluginId: 'com.example.timeout', permissions: [], - timeoutMs: 50, + // timeoutMs 同时约束 Worker 启动与调用超时:真实 Worker 线程在负载高的 + // CI 上启动可能超过极小值,导致 activate() 阶段就抛 PLUGIN_RUNTIME_TIMEOUT + //(flaky)。取 2000ms 与其他用例一致,调用超时语义不变。 + timeoutMs: 2000, }); await runtime.activate(); From fc349d24a92bf5b9b95ee146881daf8e47615010 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <libogege@sina.com> Date: Mon, 5 Oct 2026 02:36:20 +0800 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=E8=A6=86=E7=9B=96=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E6=96=87=E6=A1=A3=E4=B8=8D=E8=A7=A6=E5=8F=91=E6=95=B0?= =?UTF-8?q?=E5=AD=A6=E6=8F=92=E4=BB=B6=E5=8A=A8=E6=80=81=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit nitpick:仅断言 MATH_PATTERN 门控结果,无法发现 effect 绕过 门控直接 import 的回归。补充 passthrough 加载计数器(vi.hoisted + vi.mock),普通文档用例排空微任务后断言加载器零调用、无 .katex 节点; 用例置于 display 用例之前并注明顺序约束(vitest 缓存已解析的 mock 模块)。 🤖 Generated with [Z.ai Code](https://chat.z.ai) --- src/components/MarkdownRenderer.test.tsx | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/MarkdownRenderer.test.tsx b/src/components/MarkdownRenderer.test.tsx index 5063b0f6..340a7e55 100644 --- a/src/components/MarkdownRenderer.test.tsx +++ b/src/components/MarkdownRenderer.test.tsx @@ -22,6 +22,18 @@ vi.mock('mermaid', () => ({ }, })); +// 统计数学插件加载器被动态 import 的次数(passthrough mock,不改变真实行为): +// 普通文档用例断言计数保持为 0,覆盖「effect 绕过 MATH_PATTERN 门控直接发起加载」的回归。 +const mathLoaderCalls = vi.hoisted(() => ({ remark: 0, rehype: 0 })); +vi.mock('remark-math', async (importOriginal) => { + mathLoaderCalls.remark += 1; + return importOriginal(); +}); +vi.mock('rehype-katex', async (importOriginal) => { + mathLoaderCalls.rehype += 1; + return importOriginal(); +}); + describe('MarkdownRenderer', () => { beforeEach(() => { vi.clearAllMocks(); @@ -702,17 +714,26 @@ describe('MarkdownRenderer', () => { expect(MATH_PATTERN.test('$x^2$')).toBe(true); }); + it('should not load math support for plain documents', async () => { + // 本用例必须在下方 display-math 用例之前运行:vitest 会缓存已解析的 mock + // 模块,display 用例触发过动态 import 后计数器不再增长。 + mathLoaderCalls.remark = 0; + mathLoaderCalls.rehype = 0; + const { container } = render(<MarkdownRenderer content="Just $5 and text" />); + // 与 effect 相同的门控判断:普通文档不满足数学语法 + expect(MATH_PATTERN.test('Just $5 and text')).toBe(false); + // 排空动态导入的微任务:若 effect 绕过门控发起 import,计数器会在这里增长 + for (let i = 0; i < 10; i++) await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mathLoaderCalls.remark).toBe(0); + expect(mathLoaderCalls.rehype).toBe(0); + expect(container.querySelector('.katex')).toBeNull(); + }); + it('should lazily load KaTeX and render display math', async () => { const { container } = render(<MarkdownRenderer content="$$E=mc^2$$" />); await waitFor(() => { expect(container.querySelector('.katex')).toBeInTheDocument(); }, { timeout: 5000 }); }, 10000); - - it('should not load math support for plain documents', () => { - // KaTeX 是否加载完全由 MATH_PATTERN.test(content) 门控(与 effect 一致), - // 直接断言门控为 false,不依赖真实定时器长度的竞态。 - expect(MATH_PATTERN.test('Just $5 and text')).toBe(false); - }); }); });