fancive_obsidian-parallel-r.../tests/providers.test.js
wujunchen 7cb68658f5 refactor: split modules.test.js into 12 per-module test files
Replaces the monolithic modules.test.js (983 lines) with focused
per-module test files sharing a common test-setup.js. Each file is
under 225 lines. All existing test assertions preserved.

New files: test-setup.js, anchor.test.js, schema.test.js, cache.test.js,
cards-nav.test.js, markdown.test.js, vault-batch.test.js, i18n.test.js,
scroll.test.js, settings.test.js, providers.test.js, streaming.test.js,
cli.test.js

Change-Id: I37a3342a1588807277e679e6535cde361e650565
2026-04-27 15:21:03 +08:00

225 lines
8 KiB
JavaScript

const { assert, t, baseSettings, openAiCardsResponse, setRequestUrlMock } = require('./test-setup');
// ── prompt.ts ──
const enPrompt = t.buildPrompts('Hello world', {
...baseSettings,
promptLanguage: 'en',
minCards: 3,
maxCards: 7,
});
assert.ok(enPrompt.system.includes('3-7'), 'card range in system prompt');
assert.ok(enPrompt.system.includes('English'), 'language instruction in english prompt');
assert.ok(enPrompt.user.includes('Source document:'), 'english user prompt prefix');
assert.ok(enPrompt.user.includes('Hello world'), 'content in user prompt');
const zhPrompt = t.buildPrompts('你好世界', {
...baseSettings,
promptLanguage: 'zh',
minCards: 2,
maxCards: 5,
});
assert.ok(zhPrompt.system.includes('2-5'), 'zh card range');
assert.ok(zhPrompt.system.includes('中文'), 'chinese language instruction');
assert.ok(zhPrompt.user.includes('你好世界'), 'zh content');
const customPrompt = t.buildPrompts('doc', {
...baseSettings,
promptLanguage: 'en',
minCards: 1,
maxCards: 3,
customSystemPrompt: 'Generate {minCards} to {maxCards} cards. {languageInstruction}',
});
assert.ok(customPrompt.system.includes('Generate 1 to 3 cards'), 'custom prompt variable substitution');
assert.ok(customPrompt.system.includes('Non-overridable output contract'), 'contract appended to custom prompt');
const longDoc = 'x'.repeat(25000);
const truncated = t.buildPrompts(longDoc, {
...baseSettings,
promptLanguage: 'en',
minCards: 1,
maxCards: 3,
maxDocChars: 20000,
});
assert.ok(truncated.user.includes('[Document truncated]'), 'truncation marker');
assert.ok(truncated.user.length < 25000, 'content actually truncated');
// ── provider body builders ──
const chatBody = t.buildOpenAiChatBody('sys', 'usr', { ...baseSettings, apiMaxTokens: 200 });
assert.strictEqual(chatBody.model, 'gpt-5.1', 'model stripped');
assert.strictEqual(chatBody.messages.length, 2, 'system + user messages');
assert.ok(chatBody.response_format, 'has structured output');
const chatUnstructured = t.buildOpenAiChatBody('sys', 'usr', baseSettings, { structured: false });
assert.strictEqual(chatUnstructured.response_format, undefined, 'no structured output when disabled');
const anthBody = t.buildAnthropicMessagesBody('sys', 'usr', {
...baseSettings,
apiProvider: 'anthropic',
apiFormat: 'anthropic-messages',
model: 'claude-sonnet-4-6',
});
assert.ok(anthBody.tools, 'has tools for structured output');
assert.strictEqual(anthBody.tools[0].name, 'record_parallel_reader_cards');
assert.strictEqual(anthBody.system, 'sys', 'system prompt set');
const respBody = t.buildOpenAiResponsesBody('sys', 'usr', baseSettings);
assert.strictEqual(respBody.instructions, 'sys', 'instructions set');
assert.strictEqual(respBody.input, 'usr', 'input set');
assert.ok(respBody.text, 'has text format');
const gemBody = t.buildGeminiBody('sys', 'usr', {
...baseSettings,
apiProvider: 'google',
apiFormat: 'google-generative-ai',
model: 'gemini-3-pro-preview',
});
assert.strictEqual(gemBody.systemInstruction.parts[0].text, 'sys');
assert.strictEqual(gemBody.contents[0].parts[0].text, 'usr');
assert.strictEqual(gemBody.generationConfig.responseMimeType, 'application/json');
assert.strictEqual(
t.tokenLimitFieldForOpenAiChat({ ...baseSettings, apiProvider: 'openai' }),
'max_completion_tokens',
'OpenAI uses max_completion_tokens'
);
assert.strictEqual(
t.tokenLimitFieldForOpenAiChat({ ...baseSettings, apiProvider: 'openrouter' }),
'max_tokens',
'OpenRouter uses max_tokens'
);
// ── parseApiHeaders ──
assert.deepStrictEqual(
t.parseApiHeaders('{"X-Custom": "value", "Authorization": "Bearer tok"}'),
{ 'X-Custom': 'value', 'Authorization': 'Bearer tok' },
'parseApiHeaders: JSON object input',
);
assert.deepStrictEqual(t.parseApiHeaders(''), {}, 'parseApiHeaders: empty string returns empty object');
assert.deepStrictEqual(t.parseApiHeaders(' '), {}, 'parseApiHeaders: whitespace-only returns empty object');
assert.deepStrictEqual(
t.parseApiHeaders('X-Custom: value\nAuthorization: Bearer tok'),
{ 'X-Custom': 'value', 'Authorization': 'Bearer tok' },
'parseApiHeaders: line-based input',
);
assert.deepStrictEqual(
t.parseApiHeaders('# comment\nX-Key: val\n\n# another\nY-Key: val2'),
{ 'X-Key': 'val', 'Y-Key': 'val2' },
'parseApiHeaders: comments and blank lines skipped',
);
assert.throws(
() => t.parseApiHeaders('{ invalid json }', baseSettings),
/parse failed|JSON/i,
'parseApiHeaders: malformed JSON throws',
);
assert.throws(
() => t.parseApiHeaders('[1, 2, 3]', baseSettings),
/Header.*value|format/i,
'parseApiHeaders: JSON array input throws line format error',
);
assert.throws(
() => t.parseApiHeaders('InvalidHeader', baseSettings),
/Header.*value|format/i,
'parseApiHeaders: missing colon throws',
);
assert.deepStrictEqual(
t.parseApiHeaders('{"valid": "yes", "number": 42, "null": null}'),
{ valid: 'yes' },
'parseApiHeaders: non-string values filtered from JSON',
);
assert.deepStrictEqual(
t.parseApiHeaders('Authorization: Bearer abc:def:ghi'),
{ 'Authorization': 'Bearer abc:def:ghi' },
'parseApiHeaders: value with colons preserved',
);
// ── summarizeDocument ──
async function testSummarizeDocumentAnchorSorting() {
const prev = require('./test-setup').getRequestUrlMock();
setRequestUrlMock(async () =>
openAiCardsResponse([
{ title: 'Second', anchor: 'second anchor', gist: 'G2', bullets: ['B2'] },
{ title: 'First', anchor: 'first anchor', gist: 'G1', bullets: ['B1'] },
]),
);
let sections;
try {
sections = await t.summarizeDocument(
'intro\nfirst anchor\nmiddle\nsecond anchor\nend',
{ ...baseSettings, streaming: false, promptLanguage: 'en', minCards: 1, maxCards: 3, maxDocChars: 10000 },
{ phase: 'queued', onCancel: () => {}, throwIfCancelled: () => {}, setPhase: () => {} },
);
} finally {
setRequestUrlMock(prev);
}
assert.deepStrictEqual(sections.map((s) => s.title), ['First', 'Second'], 'sorts by anchor line');
assert.deepStrictEqual(sections.map((s) => s.startLine), [1, 3], 'resolves anchor lines');
}
async function testStructuredOutputFallback() {
const prev = require('./test-setup').getRequestUrlMock();
let callCount = 0;
setRequestUrlMock(async () => {
callCount++;
if (callCount === 1) {
return {
status: 400,
json: { error: 'response_format is not supported' },
text: JSON.stringify({ error: 'response_format is not supported' }),
};
}
return openAiCardsResponse([
{ title: 'Fallback', anchor: 'fallback anchor', gist: 'G', bullets: ['B'] },
]);
});
try {
const sections = await t.summarizeDocument(
'intro\nfallback anchor\nend',
{ ...baseSettings, streaming: false, promptLanguage: 'en', minCards: 1, maxCards: 3, maxDocChars: 10000 },
{ phase: 'queued', onCancel: () => {}, throwIfCancelled: () => {}, setPhase: () => {} },
);
assert.strictEqual(callCount, 2, 'structured output fallback made two requests');
assert.strictEqual(sections[0].title, 'Fallback', 'fallback response parsed correctly');
} finally {
setRequestUrlMock(prev);
}
}
async function testStructuredOutputNonRetryableError() {
const prev = require('./test-setup').getRequestUrlMock();
setRequestUrlMock(async () => ({
status: 500,
json: { error: 'Internal server error' },
text: JSON.stringify({ error: 'Internal server error' }),
}));
try {
await assert.rejects(
() => t.summarizeDocument(
'test content',
{ ...baseSettings, streaming: false, promptLanguage: 'en', minCards: 1, maxCards: 3, maxDocChars: 10000 },
{ phase: 'queued', onCancel: () => {}, throwIfCancelled: () => {}, setPhase: () => {} },
),
/500/,
'non-retryable error (500) is not caught by structured fallback',
);
} finally {
setRequestUrlMock(prev);
}
}
(async () => {
await testSummarizeDocumentAnchorSorting();
await testStructuredOutputFallback();
await testStructuredOutputNonRetryableError();
console.log('providers tests passed');
})().catch((e) => {
console.error(e);
process.exit(1);
});