replace forkpty() with posix_openpt() + posix_spawn() (T-96)

`forkpty` calls `fork()` underneath. `fork()` in a multithreaded
process is unsafe: only the calling thread survives in the child,
but libc locks held by other threads remain "locked forever." With
the multi-threaded Dart VM as parent, ~5% of spawns deadlocked in
the child before `execve` (forensic probe: child stuck in S state
with comm=`DartWorker`, master fd never sees POLLIN).

`posix_spawn` uses `vfork` on glibc/musl/macOS, keeping the parent
suspended until execve completes — no Dart code runs in the child.
Pty pair built via the POSIX-standard `posix_openpt` / `grantpt` /
`unlockpt` / `ptsname` sequence. Probed: zero hangs in 300
sequential spawns vs ~5% before.

Behavior change: missing executable / missing workingDirectory now
surface as a `PtyException` thrown by `NativePty.start` rather than
a diagnostic written from the child to the slave PTY. Cleaner error
path for callers.

Side benefit: drops the `libutil.so.1` dynamic-library dependency.
PTY now resolves entirely against libc via `DynamicLibrary.process()`.

Splits the library-level `@Tags(['forkpty'])` on session_test.dart
into a per-test tag, so the now-runnable-under-flutter-test cases
contribute to coverage. `dart_test.yaml` declares the tag so the
exclude-tags filters honor it. Drops the `retry: 2` workaround from
the formerly-flaky registry test.

D-5 amended. Trims session-introduced CHANGELOG entries that were
over-verbose for the Keep-a-Changelog format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 20:21:41 +02:00
co-authored by Claude Opus 4.7
parent ab2e5e618b
commit 8074bf4201
10 changed files with 291 additions and 225 deletions
+12 -30
View File
@@ -46,26 +46,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
end target is 95% (D-66). `ci/test.sh` now writes
`coverage/lcov.info` as a side effect of the unit/widget/golden
run so the gate adds no extra test invocation.
- Test sweep covering `kernel/src/commands/keybindings.dart` (KeyEvent
modifier mapping, parse-error edges, resolver entries view),
`kernel/src/toolchain_paths.dart` (the Flutter-free `ToolchainView.resolved`
static view), `widgets/src/clide_tooltip.dart` (hover-delay overlay,
flip-above placement, re-entry cycle), `widgets/src/clide_palette.dart`
(filter typing, submit-invokes-first, hover state),
`widgets/src/multitab_controller.dart` (`copyWith`, `length`/`isEmpty`
getters), and `widgets/src/clide_markdown.dart` (h3–h6, tables,
strikethrough, default block fallback, record-link tap). Crosses
the 93% line-coverage threshold (T-91).
- `kernel/src/syntax/tree_sitter_service.dart` test sweep — every
`colorForRole` switch arm plus fake-FFI coverage of
`_init`/`_loadGrammar`/`highlight`/`dispose` branches, taking the
file from 17% → 96%. Real-library smoke test
(`test/kernel/src/syntax/tree_sitter_smoke_test.dart`) dlopen's the
vendored `native/linux-x64/libtree-sitter.so`, loads the bundled
`dart` grammar end-to-end, and verifies highlight emits sane spans —
catches FFI signature drift the fake-driven tests can't. Skips on
non-Linux and when the vendored library isn't present. Drives total
line coverage across the 95% target (T-91).
- Test sweep — `keybindings`, `toolchain_paths`, and several
`widgets/src/` primitives (tooltip, palette, multitab, markdown).
- `tree_sitter_service` sweep — fake-FFI + real-library smoke,
17% → 96%. Crosses the 95% global target (T-91).
- Staged `dart doc` CI job — generates and uploads an HTML API
reference for the public `lib/` surface. The step wraps
`dart doc --validate-links` and grep-fails the build on any warning,
@@ -119,16 +103,14 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Changed
- Pre-push line-coverage floor ratcheted to 95% — D-66 target hit.
- `TreeSitterService` accepts injectable `TreeSitterLib` and grammar /
query loaders so tests can substitute a fake FFI surface without
dlopen'ing `libtree-sitter.so`. `TreeSitterLib.testing(...)` exposes a
named-parameter constructor with safe no-op defaults for every native
function; `TreeSitterLib.fromDynamicLibrary(...)` lets the smoke test
load the vendored `.so` directly. Production paths
(`TreeSitterService.shared`, `TreeSitterLib.instance`) unchanged.
- Tidied test imports — dropped redundant `dart:ui` / `dart:typed_data`
/ barrel-redundant package imports flagged by `unnecessary_import`.
- PTY spawning uses `posix_openpt` + `posix_spawn` instead of
`forkpty` — closes a ~5% deadlock window in the multithreaded Dart
VM (T-96, D-5 amended). Missing exe/cwd now throw `PtyException` at
spawn time. Drops the `libutil.so.1` dependency.
- Coverage floor ratcheted to 95% — D-66 target hit.
- `TreeSitterService` and `TreeSitterLib` accept injectable FFI + asset
loaders for fake-driven tests; production paths unchanged.
- Tidied test imports flagged by `unnecessary_import`.
- Terminal panes now render bold attributes with a real bold weight —
bundled JetBrainsMono Bold + BoldItalic are registered with the
`JetBrainsMono` family at `weight: 700`. The painter's bold