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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion lib/loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,})(.*)$/);
Expand Down
31 changes: 29 additions & 2 deletions test/language-replies.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down Expand Up @@ -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.', '',
Expand Down
Loading