Skip to content

Commit 97301db

Browse files
committed
fix(approval): treat null ask as missing context in unavailable-copy checks
Normalize null and undefined in the ask-aware unavailable-copy branches across reply, forwarder, and Control UI renderers so that explicit ask: null uses the policy-required fallback instead of the non-persistable message. Add focused null regression tests for reply and forwarder. Addresses Codex review finding on PR #97077.
1 parent cee5dee commit 97301db

5 files changed

Lines changed: 43 additions & 3 deletions

File tree

src/infra/exec-approval-forwarder.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,29 @@ describe("exec approval forwarder", () => {
667667
expect(text).not.toContain("cannot be persisted");
668668
});
669669

670+
it("shows policy-required reason when allow-always is unavailable and ask is null", async () => {
671+
vi.useFakeTimers();
672+
const { deliver, forwarder } = createForwarder({ cfg: TARGETS_CFG });
673+
await expect(
674+
forwarder.handleRequested({
675+
...baseRequest,
676+
request: {
677+
...baseRequest.request,
678+
ask: null,
679+
unavailableDecisions: ["allow-always"],
680+
},
681+
}),
682+
).resolves.toBe(true);
683+
await Promise.resolve();
684+
const text = getFirstDeliveryText(deliver);
685+
expect(text).toContain("Reply with: /approve req-1 allow-once|deny");
686+
expect(text).not.toContain("allow-once|allow-always|deny");
687+
expect(text).toContain(
688+
"Allow Always is unavailable because the effective policy requires approval every time.",
689+
);
690+
expect(text).not.toContain("cannot be persisted");
691+
});
692+
670693
it("shows non-persistable reason when allow-always is unavailable without ask=always", async () => {
671694
vi.useFakeTimers();
672695
const { deliver, forwarder } = createForwarder({ cfg: TARGETS_CFG });

src/infra/exec-approval-forwarder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export function buildExecApprovalRequestMessage(request: ExecApprovalRequest, no
294294
lines.push(`Reply with: /approve ${request.id} ${decisionText}`);
295295
if (!allowedDecisions.includes("allow-always")) {
296296
lines.push(
297-
request.request.ask !== undefined && request.request.ask !== "always"
297+
request.request.ask != null && request.request.ask !== "always"
298298
? "Allow Always is unavailable because this command cannot be persisted (e.g., shell redirection or dynamic content)."
299299
: "Allow Always is unavailable because the effective policy requires approval every time.",
300300
);

src/infra/exec-approval-reply.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,23 @@ describe("exec approval reply helpers", () => {
384384
expect(payload.text).not.toContain("cannot be persisted");
385385
});
386386

387+
it("shows policy-required reason when allow-always is unavailable and ask is null", () => {
388+
const payload = buildExecApprovalPendingReplyPayload({
389+
approvalId: "req-null-ask",
390+
approvalSlug: "slug-null-ask",
391+
ask: null,
392+
allowedDecisions: ["allow-once", "deny"],
393+
command: "echo hello",
394+
host: "gateway",
395+
});
396+
397+
expect(payload.text).not.toContain("allow-always");
398+
expect(payload.text).toContain(
399+
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
400+
);
401+
expect(payload.text).not.toContain("cannot be persisted");
402+
});
403+
387404
it("shows non-persistable reason when allow-always is unavailable without ask=always", () => {
388405
const payload = buildExecApprovalPendingReplyPayload({
389406
approvalId: "req-oneshot",

src/infra/exec-approval-reply.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ export function buildExecApprovalPendingReplyPayload(
380380
// without the full exec context), default to the policy-required explanation
381381
// rather than the non-persistable message.
382382
lines.push(
383-
params.ask !== undefined && params.ask !== "always"
383+
params.ask != null && params.ask !== "always"
384384
? "Allow Always is unavailable because this command cannot be persisted (e.g., shell redirection or dynamic content)."
385385
: "The effective approval policy requires approval every time, so Allow Always is unavailable.",
386386
);

ui/src/ui/views/exec-approval.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function renderUnavailableDecisionWarning(
156156
if (active.kind !== "exec" || decisions.includes("allow-always")) {
157157
return nothing;
158158
}
159-
const isNonPersistable = active.request.ask !== undefined && active.request.ask !== "always";
159+
const isNonPersistable = active.request.ask != null && active.request.ask !== "always";
160160
const key = isNonPersistable
161161
? "execApproval.allowAlwaysUnavailableNonPersistable"
162162
: "execApproval.allowAlwaysUnavailable";

0 commit comments

Comments
 (0)