diff --git a/CHANGELOG.md b/CHANGELOG.md index 242c00d..cfabfa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Recognize quoted dispute examples in JSON replies with CRLF line endings, + avoiding false reopened findings and preserving the correct rebuttal evidence. - Keep agreed findings settled in mixed replies with explicit dispute IDs, and stop rebuttal evidence before a separate new finding while preserving restatements of the disputed finding. diff --git a/lib/loop.js b/lib/loop.js index 18765ae..045db41 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -426,7 +426,10 @@ function disputeLine(lines, f) { function replyMarkers(lines) { const markers = []; let fence = null; - for (const [i, line] of lines.entries()) { + for (const [i, rawLine] of lines.entries()) { + // JSON report text can retain CRLF after decoding. Match Markdown syntax + // without its line terminator, keeping the original lines for excerpts. + const line = rawLine.replace(/\r$/, ""); // Four columns of indentation are a Markdown code example as well. if (/^(?: {4}| *\t)/.test(line)) continue; const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); diff --git a/test/language-replies.test.js b/test/language-replies.test.js index cb117c8..b2abe26 100644 --- a/test/language-replies.test.js +++ b/test/language-replies.test.js @@ -14,10 +14,16 @@ const {replyRound, outstanding} = await load('loop.js'); const {languageIn} = await load('language.js'); const {triagePrompt} = await load('triage.js'); -async function reply(t, claim, report, language = 'zh-CN') { +async function reply(t, claim, report, language = 'zh-CN', format = 'whole') { const dir = await mkdtemp(path.join(tmpdir(), 'jury-language-reply-')); t.after(() => rm(dir, {recursive: true, force: true})); - const agent = {name: 'fixture', argv: [process.execPath, '-e', `console.log(${JSON.stringify(report)})`]}; + const output = format === 'codex-json' ? [ + {type: 'turn.started'}, {type: 'item.completed', item: {type: 'agent_message', text: report}}, + {type: 'turn.completed'}, + ].map(event => JSON.stringify(event)).join('\n') : format === 'result-json' + ? JSON.stringify({type: 'result', subtype: 'success', is_error: false, result: report}) : report; + const agent = {name: 'fixture', report: format, + argv: [process.execPath, '-e', `console.log(${JSON.stringify(output)})`]}; await appendEvent(dir, {t: 'finding.raised', id: 'A', agent: agent.name, round: 1, claim, loc: 'value.js:1'}); await appendEvent(dir, {t: 'finding.resolved', id: 'A', verdict: 'rejected', reason: 'Checked'}); const [answer] = await replyRound({dir, pool: [agent], cfg: {stopToken: 'NO NEW FINDINGS'}, @@ -161,6 +167,27 @@ test('a three-space indented live dispute remains active', async t => { assert.equal(findings.get('A').contested, true); }); +for (const format of ['codex-json', 'result-json']) for (const eol of ['\n', '\r\n']) for (const fence of ['```', '~~~']) { + test(`fenced dispute examples remain quoted with ${format}, ${JSON.stringify(eol)} and ${fence}`, async t => { + const quoted = ['Earlier example:', `${fence}text`, 'DISPUTE: A', + 'This quoted objection is withdrawn.', fence].join(eol); + const clean = await reply(t, '[P2] Missing authorization validation', + `${quoted}${eol}I agree with the resolution.${eol}NO NEW FINDINGS`, 'zh-CN', format); + assert.equal(clean.answer.clean, true); + assert.deepEqual(clean.events.filter(e => e.t === 'finding.turn'), []); + assert.equal(clean.findings.get('A').contested, false); + + const explanation = ['DISPUTE: A', 'The alternate route still fails.', '', + 'Reproduction:', '1. Request the alternate route.', 'Observed: the incorrect response.'].join(eol); + const active = await reply(t, '[P2] Missing authorization validation', + `${quoted}${eol}${explanation}${eol}`, 'zh-CN', format); + assert.equal(active.answer.clean, false); + assert.deepEqual(active.findings.get('A').rebuttals, [explanation], + 'only the live section reaches the judge, preserving its original line endings'); + assert.deepEqual(active.events.filter(e => e.t === 'finding.turn').map(e => e.text), [explanation]); + }); +} + test('complete unmarked rebuttals retain late reproduction evidence for the judge', async t => { const report = ['Missing authorization remains unfixed.', '', 'Reproduction:', '1. Sign out.', '', '2. Request the primary route and observe 401.', '',