feat(ci): capture crash evidence as artifacts when a run wedges (T-436)

Closes the observability loop: the log + breadcrumb + watchdog files are now
collected by CI so a wedged run leaves downloadable evidence instead of
nothing.

- logDirectory(): CLIDE_LOG_DIR overrides the per-platform default, so CI can
  point the logs at an uploadable workspace dir (and tests at a temp dir).
  Now takes an injectable env map; tested.
- test_app.dart: when CLIDE_LOG_DIR is set, the testmode harness tees its
  logger to a FileLogSink + spawns the watchdog (off by default — normal
  run-testmode keeps the stderr-only path, no isolate). _say breadcrumbs each
  test into the file.
- conpty_orphan_probe.dart: with CLIDE_LOG_DIR set it passes a verbose PtyLog,
  so when soak-conpty-kill.ps1 force-kills the parent, the reader/waiter
  isolates' LAST crumb is fsynced to disk — naming what the wedged isolate was
  doing at the instant of death.
- bundle-smoke job: runs the real release app with CLIDE_LOG=debug +
  CLIDE_LOG_DIR, uploads clide-logs (watchdog heartbeat/sample + FileLogSink)
  in an always() step.
- windows-soak kill-probe job: sets CLIDE_LOG_DIR, uploads the FFI crumbs.

Coverage gate 95.08%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 10:58:48 +02:00
co-authored by Claude Opus 4.8
parent ee8c9dcc11
commit f40be4f758
9 changed files with 87 additions and 9 deletions
+24
View File
@@ -31,6 +31,7 @@ import 'kernel/kernel.dart';
import 'src/pty/ffi/libc.dart' as libc;
import 'src/daemon/pane_commands.dart';
import 'src/ipc/envelope.dart';
import 'src/ipc/paths.dart' show logDirectory;
import 'src/panes/event_sink.dart';
import 'src/panes/registry.dart';
import 'src/daemon/dispatcher.dart';
@@ -59,6 +60,7 @@ class _ClideTestAppState extends State<ClideTestApp> {
@override
void initState() {
super.initState();
_attachCrashLogging();
WidgetsBinding.instance.addPostFrameCallback((_) => _runTests());
Timer(_timeout, () {
_say('timeout reached — exiting');
@@ -66,6 +68,28 @@ class _ClideTestAppState extends State<ClideTestApp> {
});
}
/// Opt-in crash evidence for testmode (T-436): when `CLIDE_LOG_DIR` is set
/// (CI / a manual Windows repro), tee this harness's logger to a
/// FileLogSink and spawn the watchdog, so a wedged testmode run leaves the
/// same log + watchdog files the real app would. `_say` breadcrumbs each
/// test through the logger, so they land in the file too. Off by default —
/// normal `make run-testmode` keeps the stderr-only path, no isolate.
void _attachCrashLogging() {
final dir = Platform.environment['CLIDE_LOG_DIR'];
if (dir == null || dir.isEmpty) return;
final logDir = logDirectory();
try {
_logger.addSink(FileLogSink(dir: Directory(logDir)).call);
} catch (_) {}
unawaited(_spawnWatchdog(logDir));
}
Future<void> _spawnWatchdog(String logDir) async {
try {
await Isolate.spawn(watchdogEntry, ('$logDir/clide-watchdog.log', 500, 2000));
} catch (_) {}
}
Future<void> _runTests() async {
const workspace = String.fromEnvironment('CLIDE_PROJECT');
const category = String.fromEnvironment('CLIDE_TESTMODE');