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
@@ -25,6 +25,7 @@ library;
import 'dart:io';
import 'package:clide/src/pty/pty_log.dart';
import 'package:clide/src/pty/windows_pty.dart';
Future<void> main(List<String> args) async {
@@ -34,6 +35,13 @@ Future<void> main(List<String> args) async {
}
final count = args.isNotEmpty ? (int.tryParse(args.first) ?? 1) : 1;
// When CLIDE_LOG_DIR is set (the soak workflow sets it), emit FFI breadcrumbs
// so that when soak-conpty-kill.ps1 force-kills this process mid-life, the
// reader/waiter isolates' LAST crumb (e.g. "ReadFile enter") is on disk —
// CI then uploads it, naming what the reader was doing when killed (T-436).
final logDir = Platform.environment['CLIDE_LOG_DIR'];
final ptyLog = (logDir == null || logDir.isEmpty) ? PtyLog.none : PtyLog(crumbPath: '$logDir/clide-pty.crumbs.log', verbose: true);
final sessions = <WindowsPty>[];
for (var i = 0; i < count; i++) {
final s = WindowsPty.start(
@@ -44,6 +52,7 @@ Future<void> main(List<String> args) async {
columns: 80,
rows: 24,
environment: {...Platform.environment, 'TERM': 'xterm-256color'},
log: ptyLog,
);
// Drain output so the reader isolate is actively pumping, closest to a
// real live pane. Discard the bytes.