test: make temp-dir cleanup resilient to concurrent-write races

The assistant execution target tests deleted their temp HOME/workspace
dirs with a raw recursive delete in addTearDown. A background flush
(e.g. controller dispose still persisting state) can keep writing into
the dir while the delete walks it, so the delete races and fails with
"Directory not empty" (errno 39), failing the test on CI.

Route all unguarded teardown deletes through the existing
_resilientDelete helper (re-check existence + retry), and harden that
helper so its final fallback never re-throws — a temp-dir cleanup
failure must never fail a test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2026-06-30 07:06:07 +08:00
parent fe4c0ebe24
commit c89be591ad

View File

@ -322,9 +322,7 @@ void main() {
'xworkmate-no-runtime-main-home-', 'xworkmate-no-runtime-main-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final controller = _sandboxController( final controller = _sandboxController(
environmentOverride: const <String, String>{}, environmentOverride: const <String, String>{},
@ -358,9 +356,7 @@ void main() {
'xworkmate-refresh-no-session-one-', 'xworkmate-refresh-no-session-one-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final controller = _sandboxController( final controller = _sandboxController(
environmentOverride: const <String, String>{}, environmentOverride: const <String, String>{},
@ -439,9 +435,7 @@ void main() {
'xworkmate-stable-task-selection-home-', 'xworkmate-stable-task-selection-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final controller = _sandboxController( final controller = _sandboxController(
environmentOverride: const <String, String>{}, environmentOverride: const <String, String>{},
@ -1716,9 +1710,7 @@ void main() {
'xworkmate-acp-interrupt-artifacts-', 'xworkmate-acp-interrupt-artifacts-',
); );
addTearDown(() async { addTearDown(() async {
if (await localWorkspace.exists()) { await _resilientDelete(localWorkspace);
await localWorkspace.delete(recursive: true);
}
}); });
final fakeGoTaskService = _RecordingGoTaskServiceClient() final fakeGoTaskService = _RecordingGoTaskServiceClient()
..onExecuteTask = ((request) async { ..onExecuteTask = ((request) async {
@ -2017,9 +2009,7 @@ void main() {
'xworkmate-acp-handshake-interrupt-artifacts-', 'xworkmate-acp-handshake-interrupt-artifacts-',
); );
addTearDown(() async { addTearDown(() async {
if (await localWorkspace.exists()) { await _resilientDelete(localWorkspace);
await localWorkspace.delete(recursive: true);
}
}); });
final fakeGoTaskService = _RecordingGoTaskServiceClient() final fakeGoTaskService = _RecordingGoTaskServiceClient()
..updatesBeforeNextOutcome.add( ..updatesBeforeNextOutcome.add(
@ -2383,9 +2373,7 @@ void main() {
'xworkmate-background-completion-home-', 'xworkmate-background-completion-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final fakeGoTaskService = _BlockingGoTaskServiceClient(); final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController( final controller = _connectedController(
@ -2508,9 +2496,7 @@ void main() {
'xworkmate-same-prompt-home-', 'xworkmate-same-prompt-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final fakeGoTaskService = _BlockingGoTaskServiceClient(); final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController( final controller = _connectedController(
@ -2683,9 +2669,7 @@ void main() {
'xworkmate-same-prompt-empty-home-', 'xworkmate-same-prompt-empty-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final fakeGoTaskService = _BlockingGoTaskServiceClient(); final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController( final controller = _connectedController(
@ -2707,9 +2691,7 @@ void main() {
continue; continue;
} }
final directory = Directory(workspace); final directory = Directory(workspace);
if (await directory.exists()) { await _resilientDelete(directory);
await directory.delete(recursive: true);
}
} }
}); });
@ -2845,9 +2827,7 @@ void main() {
'xworkmate-terminal-failure-home-', 'xworkmate-terminal-failure-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final fakeGoTaskService = _BlockingGoTaskServiceClient(); final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController( final controller = _connectedController(
@ -2925,9 +2905,7 @@ void main() {
'xworkmate-empty-output-home-', 'xworkmate-empty-output-home-',
); );
addTearDown(() async { addTearDown(() async {
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
final fakeGoTaskService = _BlockingGoTaskServiceClient(); final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedController( final controller = _connectedController(
@ -3297,9 +3275,7 @@ void main() {
addTearDown(() async { addTearDown(() async {
fakeGoTaskService.completeAll(); fakeGoTaskService.completeAll();
controller.dispose(); controller.dispose();
if (await localHome.exists()) { await _resilientDelete(localHome);
await localHome.delete(recursive: true);
}
}); });
for ( for (
@ -4902,19 +4878,28 @@ UiFeatureManifest _defaultDesktopManifest() {
} }
Future<void> _resilientDelete(Directory dir) async { Future<void> _resilientDelete(Directory dir) async {
if (!await dir.exists()) {
return;
}
for (var attempt = 0; attempt < 8; attempt++) { for (var attempt = 0; attempt < 8; attempt++) {
if (!await dir.exists()) {
return;
}
try { try {
await dir.delete(recursive: true); await dir.delete(recursive: true);
return; return;
} catch (error) { } catch (error) {
// A background flush (e.g. controller dispose still persisting state)
// may keep writing into the temp dir, so a recursive delete can race
// and fail with "Directory not empty". Retry a few times.
debugPrint('Temporary directory delete retry: $error'); debugPrint('Temporary directory delete retry: $error');
await Future<void>.delayed(const Duration(milliseconds: 50)); await Future<void>.delayed(const Duration(milliseconds: 50));
} }
} }
await dir.delete(recursive: true); // Best-effort cleanup: never fail a test over leftover temp files; the OS
// reclaims the temp directory regardless.
try {
await dir.delete(recursive: true);
} catch (error) {
debugPrint('Giving up on temporary directory cleanup: $error');
}
} }
AppController _sandboxController({ AppController _sandboxController({