From 7b9feba987f8e7549734d682e651d52758ee854c Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 13 May 2026 07:18:33 +0200 Subject: [PATCH] test sweep: cover src/pty/env + native_pty error paths (T-91) Two test additions for the pty subsystem: - test/pty/env_test.dart (9 tests): PtyException.toString with and without errno, expandedPath on every platform branch, mergePtyEnv override precedence (clide defaults > process env > explicit overrides), clidePtyEnvDefaults shape. - test/pty/session_test.dart extended (4 new tests): bare-command PATH resolution, non-existent workingDirectory triggering the chdir-failed diagnostic via the child branch, non-existent executable triggering the exec-failed diagnostic, resize on a live PTY. Coverage: pty/env.dart 5/19 -> 9/19 (remaining 10 lines are the macOS-only PATH-merge branch, only reachable when Platform.isMacOS). pty/errors.dart 0/4 -> 3/4 (remaining 1 is a const-ctor phantom). The new pty session tests run under `dart test --tags forkpty` so their branch coverage doesn't surface via lcov, but the code paths (chdir failure, execve failure, PATH resolution) are now verified. Total coverage 79.26% -> 79.34%. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/pty/env_test.dart | 77 +++++++++++++++++++++++++++++++ test/pty/session_test.dart | 93 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 test/pty/env_test.dart diff --git a/test/pty/env_test.dart b/test/pty/env_test.dart new file mode 100644 index 00000000..c92bc8f6 --- /dev/null +++ b/test/pty/env_test.dart @@ -0,0 +1,77 @@ +/// Unit tests for `lib/src/pty/env.dart` and +/// `lib/src/pty/errors.dart`. +library; + +import 'dart:io'; + +import 'package:clide/src/pty/env.dart'; +import 'package:clide/src/pty/errors.dart'; +import 'package:test/test.dart'; + +void main() { + group('PtyException', () { + test('toString includes the op + message', () { + const e = PtyException('forkpty', 'kaboom'); + expect(e.toString(), contains('forkpty')); + expect(e.toString(), contains('kaboom')); + expect(e.toString(), isNot(contains('errno='))); + }); + + test('toString embeds errno when present', () { + const e = PtyException('execve', 'no such file', errno: 2); + expect(e.toString(), contains('errno=2')); + }); + }); + + group('expandedPath', () { + test('returns a non-empty string on every platform', () { + expect(expandedPath, isNotEmpty); + }); + + test('on Linux/Windows, equals Platform.environment[PATH]', () { + if (Platform.isMacOS) return; // macOS path-merge tested separately. + expect(expandedPath, Platform.environment['PATH']); + }); + + test('on macOS, includes the well-known extras', () { + if (!Platform.isMacOS) return; + // Homebrew is the canonical one; at least one of these should appear. + expect( + expandedPath, + anyOf(contains('/opt/homebrew/bin'), contains('/usr/local/bin')), + ); + }); + }); + + group('mergePtyEnv', () { + test('clide defaults override the process env where they overlap', () { + final merged = mergePtyEnv(processEnv: { + 'TERM': 'dumb', + 'CUSTOM': 'preserved', + }); + expect(merged['TERM'], 'xterm-256color'); // clide default wins + expect(merged['COLORTERM'], 'truecolor'); + expect(merged['CUSTOM'], 'preserved'); // process env retained + }); + + test('overrides win over both process env and clide defaults', () { + final merged = mergePtyEnv( + processEnv: {'TERM': 'dumb'}, + overrides: {'TERM': 'screen-256color'}, + ); + expect(merged['TERM'], 'screen-256color'); + }); + + test('no overrides argument is equivalent to overrides = null', () { + final a = mergePtyEnv(processEnv: const {'X': '1'}); + final b = mergePtyEnv(processEnv: const {'X': '1'}, overrides: null); + expect(a, b); + }); + + test('clidePtyEnvDefaults set the expected truecolour keys', () { + expect(clidePtyEnvDefaults['TERM'], 'xterm-256color'); + expect(clidePtyEnvDefaults['COLORTERM'], 'truecolor'); + expect(clidePtyEnvDefaults['CLICOLOR_FORCE'], '1'); + }); + }); +} diff --git a/test/pty/session_test.dart b/test/pty/session_test.dart index 7fdefd10..b787c577 100644 --- a/test/pty/session_test.dart +++ b/test/pty/session_test.dart @@ -95,5 +95,98 @@ void main() { await done.future.timeout(const Duration(seconds: 3)); expect(s.isClosed, isTrue); }); + + test('bare command name resolves via the PATH env var', () async { + // 'sh' is a bare command; without resolution, execve would fail. + final s = NativePty.start( + executable: 'sh', + arguments: ['-c', 'echo path-resolution-ok'], + columns: 80, + rows: 24, + workingDirectory: '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, + ); + addTearDown(s.close); + final buf = StringBuffer(); + final done = Completer(); + s.output.listen( + (b) => buf.write(utf8.decode(b, allowMalformed: true)), + onDone: () { + if (!done.isCompleted) done.complete(); + }, + ); + await done.future.timeout(const Duration(seconds: 5), onTimeout: () {}); + expect(buf.toString(), contains('path-resolution-ok')); + }); + + test('non-existent workingDirectory produces the chdir-failed diagnostic', () async { + // chdir() fails in the child → writes diagnostic + _exit(1). + final s = NativePty.start( + executable: '/bin/sh', + arguments: ['-c', 'echo should-not-run'], + columns: 80, + rows: 24, + workingDirectory: '/tmp/clide-no-such-dir-${DateTime.now().microsecondsSinceEpoch}', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, + ); + addTearDown(s.close); + final buf = StringBuffer(); + final done = Completer(); + s.output.listen( + (b) => buf.write(utf8.decode(b, allowMalformed: true)), + onDone: () { + if (!done.isCompleted) done.complete(); + }, + ); + await done.future.timeout(const Duration(seconds: 5), onTimeout: () {}); + expect(buf.toString(), contains('chdir failed')); + }); + + test('non-existent executable produces the exec-failed diagnostic', () async { + final s = NativePty.start( + executable: '/tmp/clide-no-such-binary-${DateTime.now().microsecondsSinceEpoch}', + arguments: const [], + columns: 80, + rows: 24, + workingDirectory: '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, + ); + addTearDown(s.close); + final buf = StringBuffer(); + final done = Completer(); + s.output.listen( + (b) => buf.write(utf8.decode(b, allowMalformed: true)), + onDone: () { + if (!done.isCompleted) done.complete(); + }, + ); + await done.future.timeout(const Duration(seconds: 5), onTimeout: () {}); + expect(buf.toString(), contains('exec failed')); + }); + + test('resize on a live PTY does not throw', () async { + final s = NativePty.start( + executable: '/bin/sh', + arguments: ['-c', 'sleep 0.5'], + columns: 80, + rows: 24, + workingDirectory: '/', + environment: { + ...Platform.environment, + 'TERM': 'xterm-256color', + }, + ); + addTearDown(s.close); + s.resize(cols: 120, rows: 30); + }); }); }