@@ -16,6 +16,7 @@ const createSelector = () => {
|
16 | 16 | function createShellHarness(params?: { |
17 | 17 | spawnCommand?: typeof import("node:child_process").spawn; |
18 | 18 | env?: Record<string, string>; |
| 19 | +maxOutputChars?: number; |
19 | 20 | }) { |
20 | 21 | const messages: string[] = []; |
21 | 22 | const chatLog = { |
@@ -40,6 +41,7 @@ function createShellHarness(params?: {
|
40 | 41 | createSelector: createSelectorSpy, |
41 | 42 | spawnCommand, |
42 | 43 | ...(params?.env ? { env: params.env } : {}), |
| 44 | + ...(params?.maxOutputChars !== undefined ? { maxOutputChars: params.maxOutputChars } : {}), |
43 | 45 | }); |
44 | 46 | return { |
45 | 47 | messages, |
@@ -112,4 +114,36 @@ describe("createLocalShellRunner", () => {
|
112 | 114 | expect(spawnOptions.env?.PATH).toBe("/tmp/bin"); |
113 | 115 | expect(harness.messages).toContain("local shell: enabled for this session"); |
114 | 116 | }); |
| 117 | + |
| 118 | +it("keeps stderr visible instead of evicting it when stdout fills the output cap", async () => { |
| 119 | +const stdout = new EventEmitter(); |
| 120 | +const stderr = new EventEmitter(); |
| 121 | +const spawnCommand = vi.fn(() => ({ |
| 122 | + stdout, |
| 123 | + stderr, |
| 124 | +on: (event: string, callback: (...args: unknown[]) => void) => { |
| 125 | +if (event === "close") { |
| 126 | +setImmediate(() => { |
| 127 | +// stdout fills the entire cap; stderr then carries the failure reason. |
| 128 | +stdout.emit("data", Buffer.from("0".repeat(20))); |
| 129 | +stderr.emit("data", Buffer.from("FATAL")); |
| 130 | +callback(0, null); |
| 131 | +}); |
| 132 | +} |
| 133 | +}, |
| 134 | +})); |
| 135 | + |
| 136 | +const harness = createShellHarness({ |
| 137 | +spawnCommand: spawnCommand as unknown as typeof import("node:child_process").spawn, |
| 138 | +maxOutputChars: 20, |
| 139 | +}); |
| 140 | + |
| 141 | +const run = harness.runLocalShellLine("!noisy"); |
| 142 | +harness.getLastSelector()?.onSelect?.({ value: "yes", label: "Yes" }); |
| 143 | +await run; |
| 144 | + |
| 145 | +// The failure reason in stderr must survive even though stdout filled the cap; |
| 146 | +// the previous head-cut kept all stdout and dropped stderr entirely. |
| 147 | +expect(harness.messages.some((m) => m.includes("FATAL"))).toBe(true); |
| 148 | +}); |
115 | 149 | }); |