From 0a76103c35ed2a27bd8f5768060b3cdc3e9d32c4 Mon Sep 17 00:00:00 2001 From: mptyl Date: Thu, 2 Jul 2026 18:34:17 +0200 Subject: [PATCH] fix(gate): empty memory phase shows a notice and auto-advances instead of an empty checklist --- .../gate/__tests__/gate_decide_empty.test.js | 16 +++++++++++++ harness/.pi/extensions/tht-gate.js | 24 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 harness/.pi/extensions/gate/__tests__/gate_decide_empty.test.js diff --git a/harness/.pi/extensions/gate/__tests__/gate_decide_empty.test.js b/harness/.pi/extensions/gate/__tests__/gate_decide_empty.test.js new file mode 100644 index 00000000..036d6985 --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/gate_decide_empty.test.js @@ -0,0 +1,16 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const { shouldSkipEmptyDecide } = require("../../tht-gate.js"); + +test("skips the widget only when empty AND allow_empty AND advance", () => { + assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: true, advance: true }), true); +}); + +test("does not skip when there are merito options", () => { + assert.equal(shouldSkipEmptyDecide({ meritCount: 2, allowEmpty: true, advance: true }), false); +}); + +test("does not skip when allow_empty is false or advance is false", () => { + assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: false, advance: true }), false); + assert.equal(shouldSkipEmptyDecide({ meritCount: 0, allowEmpty: true, advance: false }), false); +}); diff --git a/harness/.pi/extensions/tht-gate.js b/harness/.pi/extensions/tht-gate.js index 66b4938b..2ff2fa2d 100644 --- a/harness/.pi/extensions/tht-gate.js +++ b/harness/.pi/extensions/tht-gate.js @@ -267,6 +267,13 @@ export function resolveConfirmOutcome(resp) { return { kind: "unknown", choice }; } +// True when a reviewer_decide has no merito options but is allowed to close empty +// and advance (the empty memory phase F2). The gate then shows an info notice and +// auto-advances instead of presenting an empty checklist. +export function shouldSkipEmptyDecide({ meritCount, allowEmpty, advance }) { + return meritCount === 0 && !!allowEmpty && !!advance; +} + export async function emitAndWait(ctx, descriptor) { for (;;) { const value = await ctx.ui.input(JSON.stringify(descriptor), ""); @@ -524,14 +531,25 @@ export default function (pi) { const { session, title, options: opts, advance } = params; const phase = phaseId(ctx, currentPhase(ctx, session)); const toAdd = []; + const meritOptions = opts + .filter((o) => !isReserved(o.label)) + .map((o) => ({ id: o.id, label: o.label })); + if (shouldSkipEmptyDecide({ meritCount: meritOptions.length, allowEmpty: params.allow_empty ?? false, advance })) { + await ctx.ui.notify( + "Nessuna memory riutilizzabile per questa domanda — passo alla fase successiva.", + "info", + ); + advanceIfReady(ctx, session); + return textResult( + "Fase memoria vuota: nessuna decisione da registrare, avanzamento automatico alla fase successiva.", + ); + } const widget = buildMultiselectRequest({ id: `u${Date.now()}`, phase, title, allowEmpty: params.allow_empty ?? false, - options: opts - .filter((o) => !isReserved(o.label)) - .map((o) => ({ id: o.id, label: o.label })), + options: meritOptions, recommended: opts.find((o) => o.recommended)?.id ?? null, }); const resp = await emitAndWait(ctx, widget);