mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
fix(gate): a new answer replaces the earlier answer's authorship and words
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
033319e710
commit
95a8f335cf
2 files changed
+25
-1
No files matched your search
+6
-1
@@ -244,14 +244,19 @@ export function decisionRows(list: GateList, extra: Partial<DecisionRow> = {}):
|
||||
/** `--by` values: the owner typing tokens, or an agent relaying or deciding them. */
|
||||
export const ANSWERED_BY_RE = /^(human|agent:[A-Za-z0-9_.:-]+)$/;
|
||||
|
||||
/** Who answered and in whose words: a new answer replaces all of it, so an earlier answerer's fields never survive under a later one. */
|
||||
const PROVENANCE = ['answered_by', 'owner_reply', 'owner_reply_parsed', 'answer_words', 'answer_words_parsed', 'words_by', 'carried_gate_from', 'carried_gate_rev', 'carry_reason', 'carried_answered_by'] as const;
|
||||
|
||||
export function applyReply(rows: DecisionRow[], parsed: ParsedReply, answeredAt: string, answeredBy?: string, extra: Partial<DecisionRow> = {}): DecisionRow[] {
|
||||
if (!parsed.valid) return rows;
|
||||
const byId = new Map(parsed.choices.map(c => [c.id, c]));
|
||||
return rows.map(row => {
|
||||
const choice = byId.get(row.label?.toLowerCase() ?? '') ?? byId.get(row.id.slice(row.run.length + 1).replace(/^r\d+-/, ''));
|
||||
if (!choice || choice.prior || row.gate_rev !== parsed.gate_rev) return row;
|
||||
const fresh = { ...row };
|
||||
for (const k of PROVENANCE) delete (fresh as Record<string, unknown>)[k];
|
||||
return {
|
||||
...row, chosen: choice.chosen, answered_at: answeredAt, reply: parsed.reply, ...(answeredBy ? { answered_by: answeredBy } : {}), ...extra,
|
||||
...fresh, chosen: choice.chosen, answered_at: answeredAt, reply: parsed.reply, ...(answeredBy ? { answered_by: answeredBy } : {}), ...extra,
|
||||
status: choice.how === 'override' ? 'overridden' : 'approved',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -67,6 +67,25 @@ describe('whose words answered the gate (GEVA-2, P7)', () => {
|
||||
expect(r.validate(s.out).status).toBe(0);
|
||||
});
|
||||
|
||||
test('a later answer from someone else replaces the earlier answer\'s authorship and words; nothing stale survives', () => {
|
||||
const dir = path.join(r.host.base, `carry${++n}`);
|
||||
const out = path.join(dir, 'run');
|
||||
expect(r.ap(['next', '--out', out, '--plan', writePlan(dir), '--no-ui']).status).toBe(0);
|
||||
phaseVia(r, out, 'ceo', NONE);
|
||||
phaseVia(r, out, 'eng', NONE);
|
||||
const first = r.ap(['answer', '--out', out, '--gate-rev', '1', '--reply', 'p1c', '--by', 'agent:gstack-minder', '--words-by', 'agent:gstack-minder', '--owner-reply', 'p1c']);
|
||||
expect(first.status, first.stderr).toBe(0);
|
||||
const second = r.ap(['answer', '--out', out, '--gate-rev', '1', '--reply', 'p1a', '--by', 'human']);
|
||||
expect(second.status, second.stderr).toBe(0);
|
||||
expect(second.stdout).toMatch(/^APPROVED: gate_rev=1/m);
|
||||
const p1 = decisions(out).find(d => d.gate_rev === 1 && d.label === 'P1');
|
||||
expect(p1).toMatchObject({ chosen: 'a', answered_by: 'human' });
|
||||
for (const stale of ['words_by', 'answer_words', 'answer_words_parsed', 'owner_reply', 'owner_reply_parsed']) expect(p1[stale]).toBeUndefined();
|
||||
const rows = importAutoplan(out, 'feature/x', 'Garry');
|
||||
if ('code' in rows) throw new Error(rows.message);
|
||||
expect(rows[0]!.by).not.toContain('agent:gstack-minder');
|
||||
});
|
||||
|
||||
test('--words-by names the owner or the answering agent; anything else is refused', () => {
|
||||
const dir = path.join(r.host.base, `carry${++n}`);
|
||||
const out = path.join(dir, 'run');
|
||||
|
||||
Reference in new issue
Block a user