Skip to content

Commit 18aeb65

Browse files
Merge pull request #564 from corbitsdev/cl-5539-tui-test-renderers-are-never-freed
Free TUI test renderers that were leaking TextBuffers (CL-5539)
2 parents 550f5be + 05668c5 commit 18aeb65

4 files changed

Lines changed: 58 additions & 30 deletions

File tree

‎src/tui/list-modal.test.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
import { describe, expect, test } from "bun:test";
1+
import { afterEach, describe, expect, test } from "bun:test";
22

3-
import { createHarness } from "./harness.js";
3+
import { createHarness, type Harness } from "./harness.js";
44
import { runListModal } from "./list-modal.js";
55

6+
let harness: Harness | undefined;
7+
8+
afterEach(() => {
9+
harness?.destroy();
10+
harness = undefined;
11+
});
12+
613
async function mountModal(): Promise<{
714
choice: Promise<string | null>;
8-
harness: Awaited<ReturnType<typeof createHarness>>;
15+
harness: Harness;
916
}> {
10-
const harness = await createHarness({ width: 80, height: 24 });
17+
harness = await createHarness({ width: 80, height: 24 });
1118
const choice = runListModal({
1219
title: "resume session",
1320
options: [
1421
{ id: "s-1", label: "First session" },
1522
{ id: "s-2", label: "Second session" },
1623
],
17-
createRenderer: async () => harness.renderer,
24+
createRenderer: async () => harness!.renderer,
1825
});
1926
await harness.renderOnce();
2027
return { choice, harness };

‎src/tui/mouse-reporting-disabled.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* `createCliRenderer` branch runs, and assert on the options it was
88
* actually called with.
99
*/
10-
import { afterAll, describe, expect, mock, test } from "bun:test";
10+
import { afterAll, afterEach, describe, expect, mock, test } from "bun:test";
1111
import type { Harness } from "./harness.js";
1212

1313
interface CapturedRendererOptions {
@@ -49,6 +49,11 @@ afterAll(() => {
4949
mock.module("@opentui/core", () => realCore);
5050
});
5151

52+
afterEach(() => {
53+
let harness: Harness | undefined;
54+
while ((harness = mountedHarnesses.pop()) !== undefined) harness.destroy();
55+
});
56+
5257
const { runListModal } = await import("./list-modal.js");
5358
const { runProviderSetup } = await import("./provider-setup.js");
5459

@@ -71,7 +76,6 @@ describe("default renderer mount disables DEC mouse reporting (CL-5540)", () =>
7176
expect(capturedOptions).toHaveLength(1);
7277
expect(capturedOptions[0]?.useMouse).toBe(false);
7378
expect(capturedOptions[0]?.enableMouseMovement).toBe(false);
74-
mountedHarnesses.pop()?.destroy();
7579
});
7680

7781
test("runProviderSetup", async () => {

‎src/tui/provider-connect.test.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { describe, test, expect } from "bun:test";
1+
import { afterEach, describe, test, expect } from "bun:test";
22
import { mkdtemp, rm } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55

6-
import { createHarness } from "./harness.js";
6+
import { createHarness, type Harness } from "./harness.js";
77
import { connectProviderInline } from "./provider-connect.js";
88
import { loadSettings } from "../config/settings.js";
99

@@ -12,17 +12,24 @@ import { loadSettings } from "../config/settings.js";
1212
// pins that an empty key on a key-required preset is rejected here too,
1313
// rather than silently downgraded to a keyless credential.
1414
describe("connectProviderInline", () => {
15+
let harness: Harness | undefined;
16+
17+
afterEach(() => {
18+
harness?.destroy();
19+
harness = undefined;
20+
});
21+
1522
test("rejects an empty key on a key-required preset without persisting", async () => {
1623
const dir = await mkdtemp(join(tmpdir(), "provider-connect-"));
1724
const settingsPath = join(dir, "settings.json");
1825
try {
19-
const harness = await createHarness({ width: 80, height: 30 });
26+
harness = await createHarness({ width: 80, height: 30 });
2027
const resultPromise = connectProviderInline({
2128
providerId: "openai",
2229
settingsPath,
2330
localSettingsPath: join(dir, "local.json"),
2431
existing: null,
25-
createRenderer: async () => harness.renderer,
32+
createRenderer: async () => harness!.renderer,
2633
});
2734
await harness.renderOnce();
2835

‎src/tui/provider-setup.test.ts‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { describe, expect, test } from "bun:test";
1+
import { afterEach, describe, expect, test } from "bun:test";
22

3-
import { createHarness, type Harness } from "./harness.js";
3+
import { createHarness as createRawHarness, type Harness } from "./harness.js";
44
import {
55
addProviderSelectorChoices,
66
connectedAccountCount,
@@ -41,6 +41,20 @@ const EMPTY: ProviderFormValues = {
4141
oauthProfile: "",
4242
};
4343

44+
// This file mounts a fresh renderer per test; track every one so a single
45+
// afterEach can free them regardless of which assertion in a test fails.
46+
const activeHarnesses: Harness[] = [];
47+
48+
async function createHarness(opts: { width: number; height: number }): Promise<Harness> {
49+
const harness = await createRawHarness(opts);
50+
activeHarnesses.push(harness);
51+
return harness;
52+
}
53+
54+
afterEach(() => {
55+
while (activeHarnesses.length > 0) activeHarnesses.pop()!.destroy();
56+
});
57+
4458
describe("provider setup pure helpers", () => {
4559
test("only the API key may be left blank", () => {
4660
expect(stepReady("name", "")).toBe(false);
@@ -1047,23 +1061,19 @@ describe("runProviderSetup paste", () => {
10471061
const { done, harness } = await mountSetup(async (values) => {
10481062
seen = values;
10491063
});
1050-
try {
1051-
await pickRow(harness, PROVIDER_IDS, "openai");
1052-
await flush(harness);
1053-
harness.pressKey("Enter");
1054-
await harness.renderOnce();
1055-
await harness.mockInput.pasteBracketedText(key);
1056-
await harness.renderOnce();
1057-
const frame = harness.captureCharFrame();
1058-
harness.pressKey("Enter");
1059-
await harness.renderOnce();
1060-
harness.pressKey("Enter");
1061-
await harness.renderOnce();
1062-
await done;
1063-
return { values: seen, frame };
1064-
} finally {
1065-
harness.destroy();
1066-
}
1064+
await pickRow(harness, PROVIDER_IDS, "openai");
1065+
await flush(harness);
1066+
harness.pressKey("Enter");
1067+
await harness.renderOnce();
1068+
await harness.mockInput.pasteBracketedText(key);
1069+
await harness.renderOnce();
1070+
const frame = harness.captureCharFrame();
1071+
harness.pressKey("Enter");
1072+
await harness.renderOnce();
1073+
harness.pressKey("Enter");
1074+
await harness.renderOnce();
1075+
await done;
1076+
return { values: seen, frame };
10671077
}
10681078

10691079
test("a pasted key lands in the field, never echoed in the clear", async () => {

0 commit comments

Comments
 (0)