Implement full E2E, integration, component, and unit tests, and resolve all leftover tmux-to-herdr issues in UI and core scripts
This commit is contained in:
BIN
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// The tmux pane backing a session row, as reported by `sessions_detail`.
|
||||
/// The herdr pane backing a session row, as reported by `sessions_detail`.
|
||||
@immutable
|
||||
class Pane {
|
||||
final int? pid;
|
||||
|
||||
@@ -13,7 +13,7 @@ class SessionRow {
|
||||
final String name;
|
||||
final String server;
|
||||
final String status;
|
||||
final bool tmuxAlive;
|
||||
final bool herdrAlive;
|
||||
final Pane pane;
|
||||
final String? role;
|
||||
final String resumeState;
|
||||
@@ -28,7 +28,7 @@ class SessionRow {
|
||||
required this.name,
|
||||
required this.server,
|
||||
required this.status,
|
||||
required this.tmuxAlive,
|
||||
required this.herdrAlive,
|
||||
required this.pane,
|
||||
this.role,
|
||||
required this.resumeState,
|
||||
@@ -49,7 +49,7 @@ class SessionRow {
|
||||
name: json['name'] as String? ?? '?',
|
||||
server: json['server'] as String? ?? 'default',
|
||||
status: json['status'] as String? ?? '?',
|
||||
tmuxAlive: json['tmux_alive'] as bool? ?? false,
|
||||
herdrAlive: json['herdr_alive'] as bool? ?? false,
|
||||
pane: Pane.fromSessionJson(json),
|
||||
role: json['role'] as String?,
|
||||
resumeState: json['resume_state'] as String? ?? '?',
|
||||
@@ -64,3 +64,4 @@ class SessionRow {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-8
@@ -4,14 +4,14 @@ import 'drift_entry.dart';
|
||||
import 'session_row.dart';
|
||||
|
||||
/// The full parsed result of one `status.sh --json` call: the untouched
|
||||
/// pre-D8 keys (timestamp/yaml_path/tmux_sessions_alive/tmux_confirmed/
|
||||
/// pre-D8 keys (timestamp/yaml_path/herdr_sessions_alive/herdr_confirmed/
|
||||
/// drifts/actions) plus the additive `sessions_detail` -> [SessionRow].
|
||||
@immutable
|
||||
class SessionsSnapshot {
|
||||
final DateTime timestamp;
|
||||
final String yamlPath;
|
||||
final List<String> tmuxSessionsAlive;
|
||||
final bool tmuxConfirmed;
|
||||
final List<String> herdrSessionsAlive;
|
||||
final bool herdrConfirmed;
|
||||
final List<DriftEntry> drifts;
|
||||
final List<String> actions;
|
||||
final List<SessionRow> sessions;
|
||||
@@ -19,8 +19,8 @@ class SessionsSnapshot {
|
||||
const SessionsSnapshot({
|
||||
required this.timestamp,
|
||||
required this.yamlPath,
|
||||
required this.tmuxSessionsAlive,
|
||||
required this.tmuxConfirmed,
|
||||
required this.herdrSessionsAlive,
|
||||
required this.herdrConfirmed,
|
||||
required this.drifts,
|
||||
required this.actions,
|
||||
required this.sessions,
|
||||
@@ -32,11 +32,11 @@ class SessionsSnapshot {
|
||||
DateTime.tryParse(json['timestamp'] as String? ?? '')?.toUtc() ??
|
||||
DateTime.now().toUtc(),
|
||||
yamlPath: json['yaml_path'] as String? ?? '',
|
||||
tmuxSessionsAlive:
|
||||
(json['tmux_sessions_alive'] as List<dynamic>? ?? const [])
|
||||
herdrSessionsAlive:
|
||||
(json['herdr_sessions_alive'] as List<dynamic>? ?? const [])
|
||||
.map((e) => e.toString())
|
||||
.toList(growable: false),
|
||||
tmuxConfirmed: json['tmux_confirmed'] as bool? ?? false,
|
||||
herdrConfirmed: json['herdr_confirmed'] as bool? ?? false,
|
||||
drifts: (json['drifts'] as List<dynamic>? ?? const [])
|
||||
.map((e) => DriftEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList(growable: false),
|
||||
@@ -49,3 +49,4 @@ class SessionsSnapshot {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -37,9 +37,9 @@ class SessionService {
|
||||
throw StatusFetchException('preflight failed: bash is missing or not executable');
|
||||
}
|
||||
|
||||
final tmuxResult = await runCommand(['tmux', '-V'], timeout: timeout);
|
||||
if (tmuxResult.rc != 0) {
|
||||
throw StatusFetchException('preflight failed: tmux is missing or not executable');
|
||||
final herdrResult = await runCommand(['herdr', '-V'], timeout: timeout);
|
||||
if (herdrResult.rc != 0) {
|
||||
throw StatusFetchException('preflight failed: herdr is missing or not executable');
|
||||
}
|
||||
|
||||
_preflightChecked = true;
|
||||
|
||||
+11
-10
@@ -24,18 +24,18 @@ void main() {
|
||||
{
|
||||
"timestamp": "2026-07-16T11:56:54Z",
|
||||
"yaml_path": "/x/.mam/agent-sessions.yaml",
|
||||
"tmux_sessions_alive": ["a|default"],
|
||||
"tmux_confirmed": true,
|
||||
"herdr_sessions_alive": ["a|default"],
|
||||
"herdr_confirmed": true,
|
||||
"drifts": [{"class": "B", "name": "a", "msg": "registered: a"}],
|
||||
"actions": ["registered: a"],
|
||||
"sessions_detail": [
|
||||
{
|
||||
"name": "a", "server": "default", "status": "running",
|
||||
"tmux_alive": true, "cmd": "claude", "role": "creator",
|
||||
"herdr_alive": true, "cmd": "claude", "role": "creator",
|
||||
"resume_state": "yes", "job_id": "-", "job_status": "-",
|
||||
"pane_cwd": "/x", "attach_command": "tmux attach -t a",
|
||||
"pane_cwd": "/x", "attach_command": "herdr session attach a",
|
||||
"drift_classes": ["B"], "pane_pid": 123, "cmd_full": "claude --foo",
|
||||
"start_command": "tmux new-session ...", "last_visible_status": "running"
|
||||
"start_command": "herdr agent start ...", "last_visible_status": "running"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -43,7 +43,7 @@ void main() {
|
||||
|
||||
final snapshot = SessionsSnapshot.fromJson(json);
|
||||
|
||||
expect(snapshot.tmuxConfirmed, isTrue);
|
||||
expect(snapshot.herdrConfirmed, isTrue);
|
||||
expect(snapshot.drifts, hasLength(1));
|
||||
expect(snapshot.drifts.single.driftClass, 'B');
|
||||
expect(snapshot.sessions, hasLength(1));
|
||||
@@ -56,7 +56,7 @@ void main() {
|
||||
expect(row.isTerminal, isFalse);
|
||||
expect(row.pane.pid, 123);
|
||||
expect(row.pane.cmdFull, 'claude --foo');
|
||||
expect(row.attachCommand, 'tmux attach -t a');
|
||||
expect(row.attachCommand, 'herdr session attach a');
|
||||
});
|
||||
|
||||
test('SessionsSnapshot.fromJson tolerates missing optional fields', () {
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
as Map<String, dynamic>;
|
||||
final snapshot = SessionsSnapshot.fromJson(json);
|
||||
|
||||
expect(snapshot.tmuxConfirmed, isFalse);
|
||||
expect(snapshot.herdrConfirmed, isFalse);
|
||||
expect(snapshot.sessions.single.name, 'bare');
|
||||
expect(snapshot.sessions.single.resumeState, '?');
|
||||
expect(snapshot.sessions.single.pane.pid, isNull);
|
||||
@@ -85,8 +85,8 @@ void main() {
|
||||
final snapshot = await service.fetchSnapshot();
|
||||
|
||||
expect(snapshot.yamlPath, isNotEmpty);
|
||||
// sessions_detail row count must match tmux_sessions_alive's distinct names.
|
||||
final aliveNames = snapshot.tmuxSessionsAlive
|
||||
// sessions_detail row count must match herdr_sessions_alive's distinct names.
|
||||
final aliveNames = snapshot.herdrSessionsAlive
|
||||
.map((entry) => entry.split('|').first)
|
||||
.toSet();
|
||||
final detailNames = snapshot.sessions.map((s) => s.name).toSet();
|
||||
@@ -95,3 +95,4 @@ void main() {
|
||||
timeout: const Timeout(Duration(seconds: 15)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -182,7 +182,9 @@ class PtySession {
|
||||
// A. Disassociate controlling terminal
|
||||
setsid();
|
||||
|
||||
// B. Isolate from nested TMUX environments (§6.7)
|
||||
// B. Isolate from nested TMUX/Herdr environments (§6.7)
|
||||
// We must unset TMUX and TMUX_PANE to ensure the child shell is properly isolated
|
||||
// from any parent terminal multiplexer session, preventing unexpected nested behavior.
|
||||
unsetenv(tmuxNamePtr.cast<ffi.Char>());
|
||||
unsetenv(tmuxPaneNamePtr.cast<ffi.Char>());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user