mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 17:20:24 +00:00
P1: - cli: pin codex --sandbox read-only (defeat prompt injection) - settings: clear apiKey/apiHeaders on provider preset switch - main/view: render error state when LLM returns empty cards - schema/cli: do not log raw model output; UI errors expose length only P2: - cli: pass --model to Claude Code when settings.model set - settings-tab: add CLI timeout input + normalizeCliTimeoutMs (min 1s) - view: export source link uses [[path|basename]] - view: export failures surface a localized Notice P3: - schema: cardUntitled fallback now respects uiLanguage - generation-job-manager: error codes localized via main.ts - batch: settled guard on promptForBatchFolder modal - README: drop stale e2e validator install note Codex review pass: - provider-parsers: forward settings to normalizeCardsPayload - generation-job-manager: classifyGenerationError regex covers new EN/zh schema-error messages - cli: do NOT pass --model to codex (DEFAULT_SETTINGS.model is a Claude name and would break codex; codex relies on its own config) Tests: cli (codex sandbox + claude --model + classify zh/en), settings (applyApiProviderPreset isolation + normalizeCliTimeoutMs), schema (language-aware fallback title), test-exports (new symbols). Change-Id: I9f4c21f9299e1a9bf166f69f712859854482fe92
114 lines
5.6 KiB
JavaScript
114 lines
5.6 KiB
JavaScript
const { assert, t } = require('./test-setup');
|
|
|
|
const { extractJson, normalizeCardsPayload, repairTruncatedCardsJson, collectJsonObjectCandidates } = t;
|
|
|
|
// ── extractJson ──
|
|
|
|
assert.strictEqual(extractJson(''), '', 'empty text returns empty');
|
|
assert.strictEqual(extractJson('{"a":1}'), '{"a":1}', 'already-valid JSON returned as-is');
|
|
assert.strictEqual(
|
|
JSON.parse(extractJson('prefix {"cards":[]} suffix')).cards.length,
|
|
0,
|
|
'extracts JSON from surrounding text',
|
|
);
|
|
assert.strictEqual(
|
|
JSON.parse(extractJson('```json\n{"cards":[]}\n```')).cards.length,
|
|
0,
|
|
'extracts JSON from fenced code block',
|
|
);
|
|
assert.strictEqual(
|
|
JSON.parse(extractJson('text {"a":1} and {"cards":[{"title":"T"}]} end')).cards[0].title,
|
|
'T',
|
|
'picks the largest valid JSON object',
|
|
);
|
|
assert.strictEqual(extractJson('not json at all'), 'not json at all', 'non-JSON text returned as-is');
|
|
|
|
const nestedBraces = '{"cards":[{"title":"test {with} braces","anchor":"a","gist":"g","bullets":[]}]}';
|
|
assert.deepStrictEqual(JSON.parse(extractJson(nestedBraces)).cards[0].title, 'test {with} braces');
|
|
|
|
// ── repairTruncatedCardsJson ──
|
|
|
|
assert.strictEqual(repairTruncatedCardsJson('not json'), null, 'non-cards text returns null');
|
|
assert.strictEqual(repairTruncatedCardsJson(''), null, 'empty string returns null');
|
|
assert.strictEqual(repairTruncatedCardsJson('{"cards":['), null, 'no complete cards returns null');
|
|
|
|
const truncJson =
|
|
'{"cards":[{"title":"A","anchor":"a","gist":"g","bullets":["b"]},{"title":"B","anchor":"x","gist":"trunc';
|
|
const repaired = repairTruncatedCardsJson(truncJson);
|
|
assert.ok(repaired, 'truncated JSON is repaired');
|
|
const repairedParsed = JSON.parse(repaired);
|
|
assert.strictEqual(repairedParsed.cards.length, 1, 'only complete card is kept');
|
|
assert.strictEqual(repairedParsed.cards[0].title, 'A', 'salvaged card has correct title');
|
|
|
|
const twoComplete =
|
|
'{"cards":[{"title":"A","anchor":"a","gist":"g","bullets":["b"]},{"title":"B","anchor":"a2","gist":"g2","bullets":["c"]},{"title":"C","anch';
|
|
const repairedTwo = JSON.parse(repairTruncatedCardsJson(twoComplete));
|
|
assert.strictEqual(repairedTwo.cards.length, 2, 'two complete cards salvaged from three');
|
|
|
|
const salvagedCards = normalizeCardsPayload(JSON.parse(repairTruncatedCardsJson(truncJson)));
|
|
assert.strictEqual(salvagedCards.length, 1, 'repair + normalize produces valid cards');
|
|
assert.strictEqual(salvagedCards[0].title, 'A', 'salvaged card has correct fields');
|
|
|
|
const spaced = '{ "cards" : [ {"title":"A","anchor":"a","gist":"g","bullets":[]},{"title":"B","anch';
|
|
const repairedSpaced = repairTruncatedCardsJson(spaced);
|
|
assert.ok(repairedSpaced, 'repair handles whitespace in cards pattern');
|
|
assert.strictEqual(JSON.parse(repairedSpaced).cards.length, 1, 'repair with whitespace keeps complete card');
|
|
|
|
const escapedQuotes =
|
|
'{"cards":[{"title":"A \\"quoted\\"","anchor":"a","gist":"g","bullets":["line \\"1\\""]},{"title":"B","anch';
|
|
const repairedEscaped = repairTruncatedCardsJson(escapedQuotes);
|
|
assert.ok(repairedEscaped, 'repair handles escaped quotes in values');
|
|
assert.strictEqual(JSON.parse(repairedEscaped).cards[0].title, 'A "quoted"', 'escaped quotes preserved');
|
|
|
|
const bracesInStrings = '{"cards":[{"title":"func() { return }","anchor":"a","gist":"{obj}","bullets":[]},{"incomp';
|
|
const repairedBraces = repairTruncatedCardsJson(bracesInStrings);
|
|
assert.ok(repairedBraces, 'repair handles braces inside strings');
|
|
assert.strictEqual(JSON.parse(repairedBraces).cards[0].title, 'func() { return }', 'braces in string preserved');
|
|
|
|
assert.strictEqual(repairTruncatedCardsJson('{"cards":[{"title":"incomp'), null, 'all truncated cards returns null');
|
|
assert.strictEqual(repairTruncatedCardsJson('{"cards":[]'), null, 'empty truncated array returns null');
|
|
|
|
// ── collectJsonObjectCandidates ──
|
|
|
|
assert.deepStrictEqual(collectJsonObjectCandidates(''), [], 'empty string gives no candidates');
|
|
assert.deepStrictEqual(collectJsonObjectCandidates('no braces'), [], 'no braces gives no candidates');
|
|
assert.deepStrictEqual(collectJsonObjectCandidates('{"a":1}'), ['{"a":1}'], 'single object extracted');
|
|
assert.deepStrictEqual(
|
|
collectJsonObjectCandidates('{"a":1} {"b":2}'),
|
|
['{"a":1}', '{"b":2}'],
|
|
'multiple objects extracted',
|
|
);
|
|
assert.deepStrictEqual(
|
|
collectJsonObjectCandidates('{"a":{"b":1}}'),
|
|
['{"a":{"b":1}}'],
|
|
'nested objects extracted as one',
|
|
);
|
|
assert.deepStrictEqual(
|
|
collectJsonObjectCandidates('{"a":"{}"}'),
|
|
['{"a":"{}"}'],
|
|
'braces in strings handled correctly',
|
|
);
|
|
assert.deepStrictEqual(collectJsonObjectCandidates('{unclosed'), [], 'unclosed brace gives no candidates');
|
|
|
|
// ── normalizeCardsPayload ──
|
|
|
|
assert.deepStrictEqual(normalizeCardsPayload(null), [], 'null payload returns empty array');
|
|
assert.deepStrictEqual(normalizeCardsPayload({}), [], 'empty object returns empty array');
|
|
assert.deepStrictEqual(normalizeCardsPayload({ cards: [null, undefined, 42, 'str'] }), [], 'non-object items filtered');
|
|
assert.deepStrictEqual(
|
|
normalizeCardsPayload({ cards: [{ title: 'T', anchor: 'A', gist: 'G', bullets: ['B'] }] }),
|
|
[{ title: 'T', anchor: 'A', gist: 'G', bullets: ['B'] }],
|
|
'valid card passes through',
|
|
);
|
|
assert.deepStrictEqual(
|
|
normalizeCardsPayload({ cards: [{ title: 123, gist: null, bullets: [1, 'valid', null] }] }, { uiLanguage: 'zh' }),
|
|
[{ title: '(无标题)', anchor: '', gist: '', bullets: ['valid'] }],
|
|
'non-string fields get defaults (zh fallback title), non-string bullets filtered',
|
|
);
|
|
assert.deepStrictEqual(
|
|
normalizeCardsPayload({ cards: [{ title: 123, gist: null, bullets: ['only'] }] }, { uiLanguage: 'en' }),
|
|
[{ title: '(Untitled)', anchor: '', gist: '', bullets: ['only'] }],
|
|
'fallback title respects en uiLanguage',
|
|
);
|
|
|
|
console.log('schema tests passed');
|