From b02b7ecb1aeea3c38688ffe51aef84c1d172ddb1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 3 Nov 2026 20:41:11 +0800 Subject: [PATCH] fix: preserve explicit null terminal abort reasons --- CHANGELOG.md | 2 ++ src/browser.ts | 2 +- src/node.ts | 2 +- test/browser.test.ts | 21 +++++++++++++++++++++ test/node.test.ts | 10 ++++++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0b54e..7dd9482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to `@openclaw/libterminal` will be documented in this file. ### Fixed +- Preserve explicitly null cancellation reasons in browser terminal startup and Node PTY creation. + - Recheck whether the application still needs a terminal hub connection when a delayed reconnect fires. - Split large Worker output acknowledgements into decodable chunks so flow-control accounting stays valid above one MiB. diff --git a/src/browser.ts b/src/browser.ts index bb37e7b..8291da2 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -304,7 +304,7 @@ async function loadRuntimeFromModule( function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) { - throw signal.reason ?? new DOMException("The operation was aborted", "AbortError"); + throw signal.reason; } } diff --git a/src/node.ts b/src/node.ts index 873c9b6..980f59b 100644 --- a/src/node.ts +++ b/src/node.ts @@ -386,7 +386,7 @@ async function findBrowserExternalAsset(distPath: string): Promise { function throwIfAborted(signal?: AbortSignal): void { if (signal?.aborted) { - throw signal.reason ?? new Error("The operation was aborted"); + throw signal.reason; } } diff --git a/test/browser.test.ts b/test/browser.test.ts index 435c408..d8f34c1 100644 --- a/test/browser.test.ts +++ b/test/browser.test.ts @@ -67,6 +67,27 @@ describe("createGhosttyTerminal", () => { await expect(created).rejects.toBe(reason); }); + it("preserves an explicit null abort reason during startup", async () => { + const abort = new AbortController(); + const dispose = vi.fn(); + class Terminal extends TestGhosttyTerminal { + dispose = dispose; + } + await expect( + createGhosttyTerminal({ + parent: {} as HTMLElement, + runtime: { ghostty: {}, Terminal, FitAddon: TestGhosttyFitAddon } as never, + signal: abort.signal, + onStatus: ({ state }) => { + if (state === "ready") { + abort.abort(null); + } + }, + }), + ).rejects.toBeNull(); + expect(dispose).toHaveBeenCalledOnce(); + }); + it("removes the caller abort listener when the terminal is disposed", async () => { const listeners = new Set<() => void>(); const signal = { diff --git a/test/node.test.ts b/test/node.test.ts index a9db3bd..516d386 100644 --- a/test/node.test.ts +++ b/test/node.test.ts @@ -14,6 +14,16 @@ import { import { collectTerminalOutput, terminalText } from "../src/testing.js"; describe("spawnLocalPty", () => { + it("preserves an explicit null abort reason before spawning", async () => { + const abort = new AbortController(); + abort.abort(null); + const driver = new FakePtyDriver(); + await expect( + spawnLocalPty({ command: "sh", cwd: "/workspace", driver, signal: abort.signal }), + ).rejects.toBeNull(); + expect(driver.spawnCalls).toBe(0); + }); + it("exchanges UTF-8 input and observes exit through the native PTY", async ({ onTestFinished, }) => {