un-quarantine app_test open-folder hang (T-280)

The "Open Folder on a non-repo path" widget test wedged the runner ~10
minutes on a _RawReceivePort teardown hang. Root cause: project
validation shelled out to `git rev-parse` via Process.run, whose exit
ReceivePort leaks under the fake-async widget-test harness.

The KernelFixture now injects a pure-Dart `.git`-walk validator
(synchronous existsSync/typeSync, no native port), so the open-folder
flow is subprocess-free. Un-skipped the test and scoped the switcher
tap to the hat-bar ClideTappable to disambiguate it from the welcome
overlay's "clide" wordmark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:43:16 +02:00
co-authored by Claude Opus 4.8
parent 76346122aa
commit 301b6becf4
4 changed files with 50 additions and 15 deletions
+15 -15
View File
@@ -315,14 +315,16 @@ void main() {
expect(tester.takeException(), isNull);
});
// QUARANTINED (T-280): this test wedges the runner for 10 minutes — teardown
// hangs on `_RawReceivePort._handleMessage`. Pre-existing (reproduces at the
// base commit, predates the T-267 epic) and not a `Process.run`/`runAsync`
// fix away — the booted-app + open-folder path holds a native port teardown
// never drains. Skipped to keep the gate green; see T-280 for the bisection
// and the real fix (drain the leaked resource, then remove this skip).
testWidgets('Open Folder on a non-repo path surfaces the "no git repo" dialog', (tester) async {
final tmp = await Directory.systemTemp.createTemp('clide-not-a-repo-');
// T-280: this previously wedged the runner ~10 min on a `_RawReceivePort`
// teardown hang. Root cause: project validation shelled out to `git rev-parse`
// via `Process.run`, whose exit ReceivePort leaks under the widget-test
// fake-async harness. The fixture now validates with a pure-Dart `.git` walk
// (no subprocess), so the open-folder flow is subprocess-free and the test
// runs clean. The only real I/O left (creating the temp dir) is confined to
// `tester.runAsync`.
testWidgets('Open Folder on a non-repo path surfaces the "no git repo" dialog (T-280)', (tester) async {
late final Directory tmp;
await tester.runAsync(() async => tmp = await Directory.systemTemp.createTemp('clide-not-a-repo-'));
addTearDown(() => tmp.delete(recursive: true));
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('clide/window'),
@@ -331,20 +333,18 @@ void main() {
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('clide/window'), null));
await pumpApp(tester);
await tester.tap(find.text(clideName)); // switcher (no project open)
// The welcome overlay also renders a "clide" wordmark, so scope the tap to
// the hat-bar switcher button (the only ClideTappable bearing that label).
await tester.tap(find.widgetWithText(ClideTappable, clideName)); // switcher (no project open)
await tester.pump();
await tester.runAsync(() async {
await tester.tap(find.text('Open Local Project')); // picks tmp → not a repo
// Let the (unawaited) command run pickDirectory + git rev-parse.
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.tap(find.text('Open Local Project')); // picks tmp → not a repo (pure-Dart walk)
await tester.pump();
await tester.pump();
expect(find.text('No git repo found'), findsOneWidget);
await tester.tap(find.text('OK'));
await tester.pump();
expect(find.text('No git repo found'), findsNothing);
}, skip: true); // T-280: wedges the runner ~10min on a ReceivePort teardown hang (pre-existing)
});
testWidgets('Alt+F opens the application File menu', (tester) async {
await pumpApp(tester);
+26
View File
@@ -21,6 +21,7 @@ class KernelFixture {
List<String>? preloadNamespaces,
Locale? initialLocale,
Locale defaultLocale = const Locale('en', 'US'),
Future<String?> Function(String path)? onValidateProject,
}) async {
final tempDir = await Directory.systemTemp.createTemp('clide_test_');
final themes = bundledThemes ?? [_miniTheme()];
@@ -38,6 +39,11 @@ class KernelFixture {
return fake!;
},
autoStartDaemonClient: false,
// Validate projects with a pure-Dart `.git` walk instead of the default
// `git rev-parse` subprocess. A real `Process.run` under the widget-test
// fake-async harness leaks its exit ReceivePort and wedges teardown for
// ~10 minutes (T-280); `existsSync` opens no native port, so it's safe.
onValidateProject: onValidateProject ?? _walkForGitRoot,
);
return KernelFixture._(
services: services,
@@ -58,6 +64,26 @@ class KernelFixture {
}
}
/// Pure-Dart stand-in for `git rev-parse --show-toplevel`: walk up from [path]
/// looking for a `.git` directory and return the repo root, or null if none.
/// Synchronous `existsSync` deliberately — it opens no native ReceivePort, so
/// it completes cleanly under the fake-async widget-test harness where a real
/// `Process.run` would leak and hang teardown (T-280).
Future<String?> _walkForGitRoot(String path) {
var dir = Directory(path);
if (!dir.existsSync()) return Future.value(null);
while (true) {
// A `.git` directory (normal clone) or file (worktree/submodule) both mark
// a repo root — match either, like `git rev-parse` would.
if (FileSystemEntity.typeSync('${dir.path}/.git') != FileSystemEntityType.notFound) {
return Future.value(dir.path);
}
final parent = dir.parent;
if (parent.path == dir.path) return Future.value(null); // reached the fs root
dir = parent;
}
}
/// A minimal bundled theme for tests that don't care about specific
/// colors — just need the pipeline to resolve.
ThemeDefinition _miniTheme() {