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
+14 -6
View File
@@ -65,18 +65,26 @@ String socketDirectory() {
/// on logout/reboot — this is a DURABLE location. The whole point of the
/// FileLogSink is that a freeze's last breadcrumbs survive the power-cycle, so
/// the log dir must outlive a reboot.
String logDirectory() {
///
/// `CLIDE_LOG_DIR` overrides everything: CI points it at a workspace dir
/// outside the build tree so a wedged run's logs can be uploaded as an
/// artifact (T-436), and tests redirect it to a temp dir.
/// [env] defaults to [Platform.environment]; injectable for tests.
String logDirectory([Map<String, String>? env]) {
final e = env ?? Platform.environment;
final override = e['CLIDE_LOG_DIR'];
if (override != null && override.isNotEmpty) return override;
if (Platform.isWindows) {
final local = Platform.environment['LOCALAPPDATA'];
final base = (local != null && local.isNotEmpty) ? local : '${Platform.environment['USERPROFILE'] ?? r'C:\'}\\AppData\\Local';
final local = e['LOCALAPPDATA'];
final base = (local != null && local.isNotEmpty) ? local : '${e['USERPROFILE'] ?? r'C:\'}\\AppData\\Local';
return '$base\\clide\\logs';
}
if (Platform.isMacOS) {
final home = Platform.environment['HOME'] ?? '/tmp';
final home = e['HOME'] ?? '/tmp';
return '$home/Library/Logs/clide';
}
final state = Platform.environment['XDG_STATE_HOME'];
final base = (state != null && state.isNotEmpty) ? state : '${Platform.environment['HOME'] ?? '/tmp'}/.local/state';
final state = e['XDG_STATE_HOME'];
final base = (state != null && state.isNotEmpty) ? state : '${e['HOME'] ?? '/tmp'}/.local/state';
return '$base/clide/logs';
}