|
5 | 5 | * box's growth and over the overlay's own context text. |
6 | 6 | */ |
7 | 7 | import { describe, expect, test } from "bun:test"; |
| 8 | +import { EventEmitter } from "node:events"; |
8 | 9 | import { defined } from "../../testkit/defined.js"; |
| 10 | +import { wireGates } from "./gate-wire.js"; |
9 | 11 | import { makePermissionItems, withTestRenderer } from "./harness.js"; |
10 | 12 | import { appendStreamRow } from "./shell/chrome.js"; |
11 | 13 | import { createAppShell } from "./shell/index.js"; |
@@ -178,3 +180,60 @@ describe("approval overlay keeps the prompt box on screen (CL-5750)", () => { |
178 | 180 | ); |
179 | 181 | }); |
180 | 182 | }); |
| 183 | + |
| 184 | +// A long multi-line command makes the permission body taller than the host. |
| 185 | +// Only the first choice was guaranteed rows, so the prompt showed Reject alone |
| 186 | +// and Accept once sat scrolled out of view (CL-11362). |
| 187 | +const LONG_COMMAND = [ |
| 188 | + "set +e", |
| 189 | + "TARGET=/Users/operator/code/worktrees/corbits-code/stack-1559-1561-1562-1563-cl-10768", |
| 190 | + 'FIXTURE="$(mktemp -d /tmp/corbits-linear-release.XXXXXX)"', |
| 191 | + ...Array.from( |
| 192 | + { length: 28 }, |
| 193 | + (_, i) => |
| 194 | + `echo "probe step ${i}: checking the release issue list" >> "$FIXTURE/log"`, |
| 195 | + ), |
| 196 | +].join("\n"); |
| 197 | + |
| 198 | +const FIXED_CHOICES = ["Reject", "Reject and stop", "Accept once"] as const; |
| 199 | + |
| 200 | +describe("a tall approval body never hides a choice (CL-11362)", () => { |
| 201 | + for (const [columns, rows] of [ |
| 202 | + [80, 24], |
| 203 | + [100, 30], |
| 204 | + ] as const) { |
| 205 | + test(`every choice is painted for a long command at ${columns}x${rows}`, async () => { |
| 206 | + await withTestRenderer( |
| 207 | + async (h) => { |
| 208 | + const shell = createAppShell(h.renderer, { |
| 209 | + terminal: { columns, rows }, |
| 210 | + run: "idle", |
| 211 | + }); |
| 212 | + const emitter = new EventEmitter(); |
| 213 | + const dispose = wireGates(emitter, shell); |
| 214 | + try { |
| 215 | + primeSession(shell); |
| 216 | + emitter.emit("permission.gate", { |
| 217 | + id: "gate-1", |
| 218 | + request: { |
| 219 | + tool: "run_shell", |
| 220 | + action: "Run shell command", |
| 221 | + subject: LONG_COMMAND, |
| 222 | + scopes: [], |
| 223 | + }, |
| 224 | + resolve: () => undefined, |
| 225 | + }); |
| 226 | + await h.renderOnce(); |
| 227 | + await h.renderOnce(); |
| 228 | + const frame = h.captureCharFrame(); |
| 229 | + for (const choice of FIXED_CHOICES) expect(frame).toContain(choice); |
| 230 | + } finally { |
| 231 | + dispose(); |
| 232 | + shell.dispose(); |
| 233 | + } |
| 234 | + }, |
| 235 | + { width: columns, height: rows }, |
| 236 | + ); |
| 237 | + }); |
| 238 | + } |
| 239 | +}); |
0 commit comments