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:
@@ -168,7 +168,7 @@ class _Header extends StatelessWidget {
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_Pill(text: s.status.toUpperCase()),
|
||||
_Pill(text: s.tmuxAlive ? 'TMUX ALIVE' : 'TMUX DEAD'),
|
||||
_Pill(text: s.herdrAlive ? 'HERDR ALIVE' : 'HERDR DEAD'),
|
||||
if (s.role != null) _Pill(text: s.role!.toUpperCase()),
|
||||
_Pill(text: 'SERVER: ${s.server}'),
|
||||
],
|
||||
|
||||
+4
-4
@@ -47,7 +47,7 @@ class SessionTable extends StatelessWidget {
|
||||
DataColumn2(label: Text('NAME'), size: ColumnSize.L),
|
||||
DataColumn2(label: Text('SERVER'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('YAML'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('TMUX'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('HERDR'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('CMD'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('RESUME'), size: ColumnSize.S),
|
||||
DataColumn2(label: Text('JOB_ID'), size: ColumnSize.S),
|
||||
@@ -79,7 +79,7 @@ class SessionTable extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
DataCell(_StatusChip(status: s.status)),
|
||||
DataCell(_TmuxChip(alive: s.tmuxAlive)),
|
||||
DataCell(_HerdrChip(alive: s.herdrAlive)),
|
||||
DataCell(
|
||||
Text(
|
||||
s.pane.cmd ?? '?',
|
||||
@@ -150,9 +150,9 @@ class _StatusChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TmuxChip extends StatelessWidget {
|
||||
class _HerdrChip extends StatelessWidget {
|
||||
final bool alive;
|
||||
const _TmuxChip({required this.alive});
|
||||
const _HerdrChip({required this.alive});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ class _TerminalPaneState extends State<TerminalPane> {
|
||||
Future<void> _startPty() async {
|
||||
try {
|
||||
final pty = await PtySession.start(
|
||||
'tmux',
|
||||
['-L', widget.serverName, 'attach', '-t', widget.sessionName],
|
||||
'herdr',
|
||||
['-L', widget.serverName, 'session', 'attach', widget.sessionName],
|
||||
);
|
||||
if (!mounted) {
|
||||
pty.close();
|
||||
|
||||
@@ -15,7 +15,7 @@ void main() {
|
||||
name: 'demo-session',
|
||||
server: 'default',
|
||||
status: 'running',
|
||||
tmuxAlive: true,
|
||||
herdrAlive: true,
|
||||
pane: const Pane(
|
||||
pid: 123,
|
||||
cwd: '/x',
|
||||
@@ -26,7 +26,7 @@ void main() {
|
||||
resumeState: 'yes',
|
||||
jobId: '-',
|
||||
jobStatus: '-',
|
||||
attachCommand: 'tmux attach -t demo-session',
|
||||
attachCommand: 'herdr session attach demo-session',
|
||||
driftClasses: const ['B'],
|
||||
),
|
||||
];
|
||||
|
||||
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