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
3 changes: 2 additions & 1 deletion electron/plugins/pluginManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,8 @@ 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}`);
t.skip(`Symlink creation unavailable: ${error.message}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// node:test �� t.skip �芣�霈啗歲餈€�隡�甇W�靚�敹◆�曉� return
return;
}
assert.equal(manager.installFromDirectory(linked).error.code, 'PLUGIN_PACKAGE_SYMLINK');
Expand Down
20 changes: 14 additions & 6 deletions electron/plugins/pluginMarketplace.test.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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') });
}
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 4 additions & 3 deletions electron/plugins/pluginPageBridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' },
Expand Down
5 changes: 4 additions & 1 deletion electron/plugins/pluginRuntime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
//嚗laky嚗€� 2000ms 銝隞靘��湛�靚頞霂凋�銝���
timeoutMs: 2000,
});
await runtime.activate();

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion server/tests/mcp/parity.test.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down Expand Up @@ -44,6 +44,12 @@ function contract(tool: { name: string; inputSchema?: Record<string, unknown> })
}

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<string, unknown> } }) => ({
Expand Down
8 changes: 3 additions & 5 deletions server/tests/mcp/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ async function canOpenSqlite(): Promise<boolean> {
}

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', () => {
Expand Down Expand Up @@ -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');
});
});
47 changes: 36 additions & 11 deletions src/components/MarkdownRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -23,6 +22,18 @@ vi.mock('mermaid', () => ({
},
}));

// 蝏恣�啣郎�辣�蝸�刻◤�冽€� import �活�堆�passthrough mock嚗��孵���銵蛹嚗�
// �桅€�獢�靘閮€霈⊥靽�銝� 0嚗��€ffect 蝏� 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();
Expand Down Expand Up @@ -692,9 +703,30 @@ describe('MarkdownRenderer', () => {

describe('Math (KaTeX)', () => {
it('uses a Safari-compatible inline math detector', () => {
const source = readFileSync('src/components/MarkdownRenderer.tsx', 'utf8');
// �� Safari 銝�� lookbehind嚗�交閮€撖澆甇���� source嚗�
// �踹�撖寞銝芣�隞嗅���急�嚗��餅�撖� cwd ��韏€�
// 甇�� (?<= 銝��� (?<! 銝斤� lookbehind 銝€撟嗆�蝏€�
expect(MATH_PATTERN.source).not.toMatch(/\(\?<([=!])/);
// �脫迫銝箇�餈��亥€��瘚�����啣郎霂剜�隞�憿餃銝�
expect(MATH_PATTERN.test('$$E=mc^2$$')).toBe(true);
expect(MATH_PATTERN.test('\\[display\\]')).toBe(true);
expect(MATH_PATTERN.test('\\(inline\\)')).toBe(true);
expect(MATH_PATTERN.test('$x^2$')).toBe(true);
});

expect(source).not.toContain('(?<!');
it('should not load math support for plain documents', async () => {
// �祉靘�憿餃銝 display-math �其�銋�餈�嚗itest 隡�摮歇閫���� mock
// 璅∪�嚗isplay �其�閫血�餈�� 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 () => {
Expand All @@ -703,12 +735,5 @@ describe('MarkdownRenderer', () => {
expect(container.querySelector('.katex')).toBeInTheDocument();
}, { timeout: 5000 });
}, 10000);

it('should not load math support for plain documents', async () => {
const { container } = render(<MarkdownRenderer content="Just $5 and text" />);
// Give the effect a tick; no katex nodes should ever appear
await new Promise((resolve) => setTimeout(resolve, 50));
expect(container.querySelector('.katex')).toBeNull();
});
});
});
5 changes: 3 additions & 2 deletions src/components/MarkdownRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/components/ReleaseCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const renderCard = (props: Partial<Parameters<typeof ReleaseCard>[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)', () => {
Expand Down
10 changes: 5 additions & 5 deletions src/components/RepositoryReleaseSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ const renderSheet = () => render(
/>
);

// 靘扳��啣餈�皜脫� Repository Health 鈭��X嚗�輸��€��啁迅摰��研€��瑟 tag �€?
// �迨�笆 Release �∠�霂W�憿駁�摰 Release �”摰孵���踹�銝�摰�蹂葡�啜€?
// 靘扳��啣餈�皜脫� Repository Health 鈭��X嚗�輸��€��啁迅摰��研€��瑟 tag ��
// �迨�笆 Release �∠�霂W�憿駁�摰 Release �”摰孵���踹�銝�摰�蹂葡�啜€�
const releaseList = () => within(screen.getByTestId('release-list'));

describe('RepositoryReleaseSheet', () => {
Expand Down Expand Up @@ -159,15 +159,15 @@ describe('RepositoryReleaseSheet', () => {

await user.click(releaseList().getByText('v1').closest('button')!);

// �航��怠像�啁�韏漣皜脫���敺賜�嚗� ReleaseCard �?AssetLeadingIcon 銝€�湛��?
// getAllByTitle嚗imple-icons �?svg �銋蒂 <title>嚗��噬蝡?class 餈誘�箏�撅?span�?
// �航��怠像�啁�韏漣皜脫���敺賜�嚗� ReleaseCard �� AssetLeadingIcon 銝€�湛���
// getAllByTitle嚗imple-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();
Expand Down
4 changes: 3 additions & 1 deletion src/services/aiRequestLimiter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ describe('AIRateLimiter', () => {
});

it('requestsPerMinute 銝僎�� acquire 銝���', async () => {
const limiter = new AIRateLimiter({ requestsPerMinute: 2, rpmWindowMs: 500 });
// 蝒�� 2s�瘚� 200ms嚗�摰恣�嗅銝 CI 鈭辣敺芰�剜��⊿▼嚗�
// �X�蝒�餈�鈭�瘚�����踹�餈�摰�冽�閫血�撖潸����€�
const limiter = new AIRateLimiter({ requestsPerMinute: 2, rpmWindowMs: 2000 });
let acquired = 0;
const pending = Array.from({ length: 4 }, async () => {
const release = await limiter.acquire();
Expand Down
9 changes: 7 additions & 2 deletions src/services/autoSync.repoHash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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([]);
Expand All @@ -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 () => {
Expand Down
6 changes: 2 additions & 4 deletions src/services/webdavService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
1 change: 0 additions & 1 deletion src/store/__fixtures__/categoryLanguage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ describe('�蔭�掩霂剛�銵蛹 fixture嚗h/en �箇瑪嚗�', () => {
const snapshot = buildSnapshot();
if (shouldUpdateFixtures()) {
writeFixture(FIXTURE_FILE, snapshot);
expect(snapshot).not.toBeNull();
return;
}
expect(snapshot).toEqual(readFixture(FIXTURE_FILE));
Expand Down
8 changes: 5 additions & 3 deletions src/store/persistence/xTweetAuthPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
// �曉��剛��蔭�∩辣嚗igrate 蝻箏仃�嗆�霂�憿餃仃韐伐����舫�暺歲餈撉�
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);
});
});
3 changes: 2 additions & 1 deletion src/utils/abortUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Loading