Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ C++ declarations:
- Run (after `npm ci`): `npm run build:libghostty && npm run build:native && npm test`
- Every native test skips when the addon does not load (`test/native-support.ts:nativeSupport`), so check the test output for skips.
- `mise run ci-ubsan` builds the addon with UBSan (Linux, GCC), then runs the tests and smoke. Undefined behavior aborts the run. The task also fails when the addon's object files contain no UBSan checks, which means the flags did not reach g++ (`mise.toml:tasks.ci-ubsan`).
- `test/workers.test.ts` checks the per-environment state in child processes with `worker_threads`. `test/terminal.test.ts` covers `feed`, `snapshot`, `resize`, `dispose`, the `RangeError` cases of the `createTerminal` checks in the addon, the `encodeMouse` bytes of each mouse format, `cursorVisible`, the cell style fields, DECSTR, and whether `scrollbackLimit` keeps history. `test/snapshot-safety.test.ts` checks in child processes that `Object.prototype` accessors cannot dispose the terminal mid-`snapshot`. `test/mouse-safety.test.ts` runs the `encodeMouse` limits and the getter re-entry cases against both the package API and the raw addon object. It skips when the addon is missing, like the other native suites. Its child-process cases load `src/` through `tsx`, so they need no `dist/` build. No vitest test covers `formatPlain`, `formatHtml`, the content of `scrollbackLines`, or the typed-array errors.
- `test/workers.test.ts` checks the per-environment state in child processes with `worker_threads`. `test/terminal.test.ts` covers `feed`, `snapshot`, `resize`, `dispose`, the `RangeError` cases of the `createTerminal` checks in the addon, the `encodeMouse` bytes of each mouse format, `cursorVisible`, the cell style fields, DECSTR, and whether `scrollbackLimit` keeps history. `test/snapshot-safety.test.ts` checks in child processes that `Object.prototype` accessors cannot dispose the terminal mid-`snapshot`. `test/mouse-safety.test.ts` runs the `encodeMouse` limits and the getter re-entry cases against both the package API and the raw addon object. It skips when the addon is missing, like the other native suites. Its child-process cases load `src/` through `tsx`, so they need no `dist/` build. No vitest test covers `formatPlain`, `formatHtml`, or the content of `scrollbackLines`.
- For changes to `TerminalWrap` or `InitAll`, also run the TLA+ lifecycle model: [`.agents/skills/verify-native-lifecycle/SKILL.md`](https://github.com/coder/libghostty-vt-node/blob/main/.agents/skills/verify-native-lifecycle/SKILL.md).
4 changes: 0 additions & 4 deletions native/terminal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,6 @@ Napi::Value TerminalWrap::Feed(const Napi::CallbackInfo& info) {
string_data = info[0].As<Napi::String>().Utf8Value();
data = reinterpret_cast<const uint8_t*>(string_data.data());
len = string_data.size();
} else if (info[0].IsBuffer()) {
const auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
data = buffer.Data();
len = buffer.Length();
} else if (info[0].IsTypedArray()) {
const auto typed_array = info[0].As<Napi::TypedArray>();
if (typed_array.TypedArrayType() != napi_uint8_array) {
Expand Down
33 changes: 33 additions & 0 deletions test/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,39 @@ describeIfNative("GhosttyVtTerminal", () => {
expect((thrown as Error).message).toMatch(message);
});

it.each<[string, () => unknown, RegExp]>([
["Uint16Array", () => new Uint16Array([0x4241]), /^feed typed array must be Uint8Array$/],
["Float32Array", () => new Float32Array([1]), /^feed typed array must be Uint8Array$/],
["Uint8ClampedArray", () => new Uint8ClampedArray([65, 66]), /^feed typed array must be Uint8Array$/],
["DataView", () => new DataView(new Uint8Array([65, 66]).buffer), /^feed data must be a string, Buffer, or Uint8Array$/],
])("rejects a %s in the addon feed with a TypeError", (_name, makeData, message) => {
const term = loadNative().createTerminal({ cols: 10, rows: 1 });
let thrown: unknown;
try {
term.feed(makeData() as Uint8Array);
} catch (error) {
thrown = error;
} finally {
term.dispose();
}
expect(thrown).toBeInstanceOf(TypeError);
expect((thrown as Error).message).toMatch(message);
});

it.each<[string, () => Uint8Array]>([
["Buffer", () => Buffer.from("AB")],
["Uint8Array", () => new Uint8Array([65, 66])],
["Buffer slice with a byte offset", () => Buffer.from("xxABxx").subarray(2, 4)],
])("writes a %s in the addon feed", (_name, makeData) => {
const term = loadNative().createTerminal({ cols: 10, rows: 1 });
try {
term.feed(makeData());
expect(term.getVisibleText()).toBe("AB");
} finally {
term.dispose();
}
});

it("makes dispose idempotent and rejects use after dispose", () => {
const term = createTerminal({ cols: 80, rows: 24 });
term.dispose();
Expand Down
Loading