Files
jackwener__opencli/clis/yuanbao/ask.test.js
jakevin d2974a9ff6 refactor(adapters): convert adapter layer from TypeScript to JavaScript (#928)
* refactor(adapters): convert adapter layer from TypeScript to JavaScript

Core framework stays TypeScript; adapter layer moves to JS-first.
Adapters are essentially "executable config + browser scripts" that
barely use TS features — this simplifies the build/distribution pipeline
by removing the dist/clis/ intermediate compilation step.

Changes:
- Convert all 753 adapter files in clis/ from .ts to .js
- Update tsconfig to exclude clis/ from compilation
- Simplify build-manifest to scan clis/*.js directly (no dist/clis/)
- Update discovery, main, fetch-adapters to load JS adapters from clis/
- Update generate-verified to output .js artifacts
- Update package.json files field: dist/clis/ → clis/
- Fix all test files for the .ts → .js transition

* fix(main): use findPackageRoot for BUILTIN_CLIS path

The previous relative path (../../clis from __dirname) only worked for
dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname
is <repo>/src — resolving to /clis instead of <repo>/clis.

Use findPackageRoot() which works for both dev and prod paths.
2026-04-10 14:52:18 +08:00

125 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest';
import { AuthRequiredError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
import { __test__ } from './ask.js';
import { askCommand } from './ask.js';
describe('yuanbao ask helpers', () => {
describe('isOnYuanbao', () => {
const fakePage = (url) => ({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) });
it('returns true for yuanbao.tencent.com URLs', async () => {
expect(await __test__.isOnYuanbao(fakePage('https://yuanbao.tencent.com/'))).toBe(true);
expect(await __test__.isOnYuanbao(fakePage('https://yuanbao.tencent.com/chat/abc'))).toBe(true);
});
it('returns false for non-yuanbao domains', async () => {
expect(await __test__.isOnYuanbao(fakePage('https://example.com/?next=yuanbao.tencent.com'))).toBe(false);
expect(await __test__.isOnYuanbao(fakePage('about:blank'))).toBe(false);
});
it('returns false when evaluate throws', async () => {
expect(await __test__.isOnYuanbao(fakePage(new Error('detached')))).toBe(false);
});
});
it('removes echoed prompt prefixes from transcript additions', () => {
expect(__test__.sanitizeYuanbaoResponseText('你好\n你好,我是元宝。', '你好')).toBe('你好,我是元宝。');
});
it('filters transient in-progress assistant placeholders', () => {
expect(__test__.sanitizeYuanbaoResponseText('正在搜索资料', '张雪机车相关的股票有哪些?')).toBe('');
});
it('normalizes boolean flags with explicit defaults', () => {
expect(__test__.normalizeBooleanFlag(undefined, true)).toBe(true);
expect(__test__.normalizeBooleanFlag(undefined, false)).toBe(false);
expect(__test__.normalizeBooleanFlag('true', false)).toBe(true);
expect(__test__.normalizeBooleanFlag('1', false)).toBe(true);
expect(__test__.normalizeBooleanFlag('yes', false)).toBe(true);
expect(__test__.normalizeBooleanFlag('false', true)).toBe(false);
});
it('ignores baseline lines and echoed prompts when collecting additions', () => {
const response = __test__.collectYuanbaoTranscriptAdditions(['旧消息'], ['旧消息', '你好', '你好\n你好,我是元宝。'], '你好');
expect(response).toBe('你好,我是元宝。');
});
it('prefers fresh assistant messages over echoed prompts and older messages', () => {
const response = __test__.pickLatestYuanbaoAssistantCandidate(['旧回复', '你好', '你好!我是元宝,由腾讯推出的AI助手。'], 1, '你好');
expect(response).toBe('你好!我是元宝,由腾讯推出的AI助手。');
});
it('converts assistant html tables to markdown tables via turndown', () => {
const markdown = __test__.convertYuanbaoHtmlToMarkdown(`
<h3>核心产业链概念股一览</h3>
<table>
<thead>
<tr><th>细分赛道</th><th>核心标的</th></tr>
</thead>
<tbody>
<tr><td>光模块</td><td>中际旭创</td></tr>
</tbody>
</table>
`);
expect(markdown).toContain('### 核心产业链概念股一览');
expect(markdown).toContain('| 细分赛道 | 核心标的 |');
expect(markdown).toContain('| --- | --- |');
expect(markdown).toContain('| 光模块 | 中际旭创 |');
});
it('tracks stabilization by incrementing repeats and resetting on changes', () => {
expect(__test__.updateStableState('', 0, '第一段')).toEqual({
previousText: '第一段',
stableCount: 0,
});
expect(__test__.updateStableState('第一段', 0, '第一段')).toEqual({
previousText: '第一段',
stableCount: 1,
});
expect(__test__.updateStableState('第一段', 1, '第二段')).toEqual({
previousText: '第二段',
stableCount: 0,
});
});
});
function createAskPageMock(overrides = {}) {
const currentUrl = overrides.currentUrl ?? 'https://yuanbao.tencent.com/';
const hasLoginGate = overrides.hasLoginGate ?? false;
const sendResult = overrides.sendResult;
return {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockImplementation(async (script) => {
if (script === 'window.location.href')
return currentUrl;
if (script.includes('微信扫码登录'))
return hasLoginGate;
if (script.includes('[dt-button-id="internet_search"]'))
return { found: false, enabled: false };
if (script.includes('[dt-button-id="deep_think"]'))
return { found: false, enabled: false };
if (script.includes('.agent-chat__list__item--ai'))
return [];
if (script.includes('const stopLines = new Set(['))
return [];
if (script.includes('Failed to insert the prompt into the Yuanbao composer.')) {
return sendResult ?? { ok: true, action: 'click' };
}
throw new Error(`Unexpected evaluate script in test: ${script.slice(0, 80)}`);
}),
};
}
describe('yuanbao ask command', () => {
it('throws AuthRequiredError when Yuanbao shows a login gate before sending', async () => {
const page = createAskPageMock({ hasLoginGate: true });
await expect(askCommand.func(page, { prompt: '你好', timeout: '60', search: true, think: false }))
.rejects.toBeInstanceOf(AuthRequiredError);
});
it('throws CommandExecutionError when the prompt cannot be sent', async () => {
const page = createAskPageMock({
sendResult: {
ok: false,
reason: 'Yuanbao composer was not found.',
},
});
await expect(askCommand.func(page, { prompt: '你好', timeout: '60', search: true, think: false }))
.rejects.toBeInstanceOf(CommandExecutionError);
});
it('throws TimeoutError when no response arrives before timeout', async () => {
const page = createAskPageMock({
sendResult: { ok: true, action: 'click' },
});
await expect(askCommand.func(page, { prompt: '你好', timeout: '-1', search: true, think: false }))
.rejects.toBeInstanceOf(TimeoutError);
});
});